angularjs-rails-resource 0.2.2 → 0.2.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +8 -8
- data/CHANGELOG.md +13 -0
- data/Gruntfile.js +95 -0
- data/README.md +66 -64
- data/angularjs-rails-resource.js +1052 -0
- data/angularjs-rails-resource.min.js +7 -0
- data/angularjs-rails-resource.zip +0 -0
- data/bower.json +28 -0
- data/lib/angularjs-rails-resource/version.rb +1 -1
- data/package.json +30 -7
- data/test/unit/angularjs/rails/resourceSpec.js +7 -0
- data/vendor/assets/javascripts/angularjs/rails/resource/serialization.js +4 -0
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
YTFlYThkZWUwZGQzZmYwMmI3ODA0MmFhMTJjODQxNzg0MzEwMWZiYQ==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
6
|
+
M2I1ZTlkNGE4MzlhYTQ1YzdiYTUwODZlMzFlMGVkNzRiYTU0ZTMzYg==
|
7
7
|
!binary "U0hBNTEy":
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
ZTNhNjViNjAzNDNkY2JkNzgwYWE5MjBmMGViZDkzNjBmOWM2YTY0YjFiNmJj
|
10
|
+
NzkxYTE5Y2U5NTgwMjJhOTY1NTE3Yjk5ZjMxMDY4MzU2MzE3NmUxNDE4YWZj
|
11
|
+
OTlhZGYxZDU3MWZlZTUwZjNlMGVkMjg5OTE4MjRkOWE1YWZjYTM=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
ZjNlY2FkMDA4MDhlMTJhMzEyYzkxNWEyMmFhODE1Y2ExMTIzODFmNGU5ODY5
|
14
|
+
M2NmZTJmMGQ1ZDc0ZTE5ZjA1OTE2NWFhY2VhODY4YjgyY2I5ZGZkNGVhMmUy
|
15
|
+
Y2NiYmJkNjEzMGQ4YzZkMzUyZTgwMTFmNDMyMzgyNWUxZDJkNDk=
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,16 @@
|
|
1
|
+
<a name="0.2.3"></a>
|
2
|
+
# 0.2.3
|
3
|
+
## Bug Fixes
|
4
|
+
- Issue #67 incorrect date deserialization led to errors constructing a new resource with a property that was type Date
|
5
|
+
|
6
|
+
<a name="0.2.2"></a>
|
7
|
+
# 0.2.2
|
8
|
+
## Bug Fixes
|
9
|
+
|
10
|
+
## Features
|
11
|
+
- Added updateMethod configuration option to railsResourceFactory to specify what HTTP method should be used to perform the update action
|
12
|
+
- Exposed default configuration options for both railsResourceFactory and railsSerializer as provider configuration options.
|
13
|
+
|
1
14
|
<a name="0.2.1"></a>
|
2
15
|
# 0.2.1
|
3
16
|
## Bug Fixes
|
data/Gruntfile.js
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
module.exports = function(grunt) {
|
2
|
+
|
3
|
+
var path = require('path');
|
4
|
+
|
5
|
+
var srcFolder = 'vendor/assets/javascripts/angularjs/rails/resource/',
|
6
|
+
srcFiles = ["index.js", "utils/**/*.js", "serialization.js", "resource.js"].map(function(glob) {
|
7
|
+
return srcFolder + glob;
|
8
|
+
});
|
9
|
+
|
10
|
+
grunt.initConfig({
|
11
|
+
pkg: grunt.file.readJSON('package.json'),
|
12
|
+
meta: {
|
13
|
+
banner: '/**\n' +
|
14
|
+
' * <%= pkg.description %>\n' +
|
15
|
+
' * @version v<%= pkg.version %> - <%= grunt.template.today("yyyy-mm-dd") %>\n' +
|
16
|
+
' * @link <%= pkg.homepage %>\n' +
|
17
|
+
' * @author <%= pkg.author %>\n' +
|
18
|
+
' */\n'
|
19
|
+
},
|
20
|
+
dirs: {
|
21
|
+
dest: './'
|
22
|
+
},
|
23
|
+
concat: {
|
24
|
+
options: {
|
25
|
+
banner: "<%= meta.banner %>"
|
26
|
+
},
|
27
|
+
dist: {
|
28
|
+
src: srcFiles,
|
29
|
+
dest: '<%= dirs.dest %>/<%= pkg.name %>.js',
|
30
|
+
options: {
|
31
|
+
process: function(src, filepath) {
|
32
|
+
return src.replace(/^\/\/= require.*\n/gm, '');
|
33
|
+
},
|
34
|
+
},
|
35
|
+
}
|
36
|
+
},
|
37
|
+
zip: {
|
38
|
+
'<%= dirs.dest %>/angularjs-rails-resource.zip': ['<%= dirs.dest %>/<%= pkg.name %>.js', '<%= dirs.dest %>/<%= pkg.name %>.min.js']
|
39
|
+
},
|
40
|
+
uglify: {
|
41
|
+
options: {
|
42
|
+
banner: "<%= meta.banner %>"
|
43
|
+
},
|
44
|
+
dist: {
|
45
|
+
files: {
|
46
|
+
'<%= dirs.dest %>/<%= pkg.name %>.min.js': ['<%= concat.dist.dest %>']
|
47
|
+
}
|
48
|
+
}
|
49
|
+
},
|
50
|
+
jshint: {
|
51
|
+
files: ['gruntfile.js'],
|
52
|
+
options: {
|
53
|
+
// options here to override JSHint defaults
|
54
|
+
globals: {
|
55
|
+
angular: true
|
56
|
+
}
|
57
|
+
}
|
58
|
+
},
|
59
|
+
watch: {
|
60
|
+
files: ['<%= jshint.files %>'],
|
61
|
+
tasks: ['jshint', 'qunit']
|
62
|
+
}
|
63
|
+
});
|
64
|
+
|
65
|
+
grunt.loadNpmTasks('grunt-contrib-uglify');
|
66
|
+
grunt.loadNpmTasks('grunt-contrib-jshint');
|
67
|
+
grunt.loadNpmTasks('grunt-contrib-watch');
|
68
|
+
grunt.loadNpmTasks('grunt-contrib-concat');
|
69
|
+
grunt.loadNpmTasks('grunt-zip');
|
70
|
+
|
71
|
+
grunt.registerTask('default', ['jshint', 'concat', 'uglify', 'zip']);
|
72
|
+
|
73
|
+
// Provides the "bump" task.
|
74
|
+
grunt.registerTask('bump', 'Increment version number', function() {
|
75
|
+
var versionType = grunt.option('type');
|
76
|
+
function bumpVersion(version, versionType) {
|
77
|
+
var type = {patch: 2, minor: 1, major: 0},
|
78
|
+
parts = version.split('.'),
|
79
|
+
idx = type[versionType || 'patch'];
|
80
|
+
parts[idx] = parseInt(parts[idx], 10) + 1;
|
81
|
+
while(++idx < parts.length) { parts[idx] = 0; }
|
82
|
+
return parts.join('.');
|
83
|
+
}
|
84
|
+
var version;
|
85
|
+
function updateFile(file) {
|
86
|
+
var json = grunt.file.readJSON(file);
|
87
|
+
version = json.version = bumpVersion(json.version, versionType || 'patch');
|
88
|
+
grunt.file.write(file, JSON.stringify(json, null, ' '));
|
89
|
+
}
|
90
|
+
updateFile('package.json');
|
91
|
+
updateFile('bower.json');
|
92
|
+
grunt.log.ok('Version bumped to ' + version);
|
93
|
+
});
|
94
|
+
|
95
|
+
};
|
data/README.md
CHANGED
@@ -25,13 +25,13 @@ Make sure to check the [CHANGELOG](CHANGELOG.md) for any breaking changes betwee
|
|
25
25
|
## Installation
|
26
26
|
|
27
27
|
Add this line to your application's Gemfile:
|
28
|
-
|
29
|
-
|
30
|
-
|
28
|
+
```ruby
|
29
|
+
gem 'angularjs-rails-resource', '~> 0.2.0'
|
30
|
+
```
|
31
31
|
Include the javascript somewhere in your asset pipeline:
|
32
|
-
|
33
|
-
|
34
|
-
|
32
|
+
```javascript
|
33
|
+
//= require angularjs/rails/resource
|
34
|
+
```
|
35
35
|
|
36
36
|
## Dependencies
|
37
37
|
Since this is an [AngularJS](http://angularjs.org) module it of course depends on that but more specifically the it depends on the following AngularJS services:
|
@@ -50,68 +50,68 @@ at [Employee Training Tracker](https://github.com/FineLinePrototyping/employee-t
|
|
50
50
|
|
51
51
|
### Basic Example
|
52
52
|
In order to create a Book resource, we would first define the factory within a module.
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
53
|
+
```javascript
|
54
|
+
angular.module('book.services', ['rails']);
|
55
|
+
angular.module('book.services').factory('Book', ['railsResourceFactory', function (railsResourceFactory) {
|
56
|
+
return railsResourceFactory({url: '/books', name: 'book'});
|
57
|
+
}]);
|
58
|
+
```
|
59
59
|
We would then inject that service into a controller:
|
60
|
+
```javascript
|
61
|
+
angular.module('book.controllers').controller('BookShelfCtrl', ['$scope', 'Book', function ($scope, Book) {
|
62
|
+
$scope.searching = true;
|
63
|
+
// Find all books matching the title
|
64
|
+
$scope.books = Book.query({title: title});
|
65
|
+
$scope.books.then(function(results) {
|
66
|
+
$scope.searching = false;
|
67
|
+
}, function (error) {
|
68
|
+
$scope.searching = false;
|
69
|
+
});
|
60
70
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
$scope.searching = false;
|
67
|
-
}, function (error) {
|
68
|
-
$scope.searching = false;
|
69
|
-
});
|
70
|
-
|
71
|
-
// Find a single book and update it
|
72
|
-
Book.get(1234).then(function (book) {
|
73
|
-
book.lastViewed = new Date();
|
74
|
-
book.update();
|
75
|
-
});
|
76
|
-
|
77
|
-
// Create a book and save it
|
78
|
-
new Book({title: 'Gardens of the Moon', author: 'Steven Erikson', isbn: '0-553-81957-7'}).create();
|
79
|
-
}]);
|
71
|
+
// Find a single book and update it
|
72
|
+
Book.get(1234).then(function (book) {
|
73
|
+
book.lastViewed = new Date();
|
74
|
+
book.update();
|
75
|
+
});
|
80
76
|
|
77
|
+
// Create a book and save it
|
78
|
+
new Book({title: 'Gardens of the Moon', author: 'Steven Erikson', isbn: '0-553-81957-7'}).create();
|
79
|
+
}]);
|
80
|
+
```
|
81
81
|
### Serializer
|
82
82
|
When defining a resource, you can pass a custom [serializer](#serializers) using the <code>serializer</code> configuration option.
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
83
|
+
```javascript
|
84
|
+
Author = railsResourceFactory({
|
85
|
+
url: '/authors',
|
86
|
+
name: 'author',
|
87
|
+
serializer: railsSerializer(function () {
|
88
|
+
this.exclude('birthDate', 'books');
|
89
|
+
this.nestedAttribute('books');
|
90
|
+
this.resource('books', 'Book');
|
91
|
+
})
|
92
|
+
});
|
93
|
+
```
|
94
94
|
You can also specify a serializer as a factory and inject it as a dependency.
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
});
|
95
|
+
```javascript
|
96
|
+
angular.module('rails').factory('BookSerializer', function(railsSerializer) {
|
97
|
+
return railsSerializer(function () {
|
98
|
+
this.exclude('publicationDate', 'relatedBooks');
|
99
|
+
this.rename('ISBN', 'isbn');
|
100
|
+
this.nestedAttribute('chapters', 'notes');
|
101
|
+
this.serializeWith('chapters', 'ChapterSerializer');
|
102
|
+
this.add('numChapters', function (book) {
|
103
|
+
return book.chapters.length;
|
105
104
|
});
|
106
105
|
});
|
106
|
+
});
|
107
107
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
108
|
+
Book = railsResourceFactory({
|
109
|
+
url: '/books',
|
110
|
+
name: 'book',
|
111
|
+
serializer: 'BookSerializer'
|
112
|
+
});
|
114
113
|
|
114
|
+
```
|
115
115
|
## Resource Creation
|
116
116
|
Creating a resource using this factory is similar to using $resource, you just call <code>railsResourceFactory</code> with the config options and it returns a new resource function.
|
117
117
|
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
|
@@ -167,11 +167,13 @@ The URL can be specified as one of three ways:
|
|
167
167
|
|
168
168
|
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}}
|
169
169
|
|
170
|
-
|
171
|
-
|
170
|
+
```javascript
|
171
|
+
Item.query({category: 'Software'}, {storeId: 123}) // would generate a GET to /stores/1234/items?category=Software
|
172
|
+
Item.get({storeId: 123, id: 1}) // would generate a GET to /stores/123/items/1
|
172
173
|
|
173
|
-
|
174
|
-
|
174
|
+
new Item({store: 123}).create() // would generate a POST to /stores/123/items
|
175
|
+
new Item({id: 1, storeId: 123}).update() // would generate a PUT to /stores/123/items/1
|
176
|
+
```
|
175
177
|
|
176
178
|
## Promises
|
177
179
|
[$http documentation](http://docs.angularjs.org/api/ng.$http) describes the promise data very well so I highly recommend reading that.
|
@@ -198,7 +200,7 @@ Resources created using <code>railsResourceFactory</code> have the following cla
|
|
198
200
|
* **context** {*} (optional) - A context object that is used during url evaluation to resolve expression variables
|
199
201
|
* **returns** {promise} - A promise that will be resolved with an array of new Resource instances
|
200
202
|
|
201
|
-
* get(context) -
|
203
|
+
* get(context) - Executes a GET request against the resource's url (e.g. /books/1234).
|
202
204
|
* **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.
|
203
205
|
* **returns** {promise} A promise that will be resolved with a new instance of the Resource
|
204
206
|
|
@@ -423,4 +425,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
423
425
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
424
426
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
425
427
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
426
|
-
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
428
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
@@ -0,0 +1,1052 @@
|
|
1
|
+
/**
|
2
|
+
* A resource factory inspired by $resource from AngularJS
|
3
|
+
* @version v0.2.3 - 2013-09-28
|
4
|
+
* @link https://github.com/FineLinePrototyping/angularjs-rails-resource.git
|
5
|
+
* @author
|
6
|
+
*/
|
7
|
+
|
8
|
+
(function (undefined) {
|
9
|
+
angular.module('rails', ['ng']);
|
10
|
+
}());
|
11
|
+
|
12
|
+
|
13
|
+
|
14
|
+
(function (undefined) {
|
15
|
+
angular.module('rails').factory('RailsInflector', function() {
|
16
|
+
function camelize(key) {
|
17
|
+
if (!angular.isString(key)) {
|
18
|
+
return key;
|
19
|
+
}
|
20
|
+
|
21
|
+
// should this match more than word and digit characters?
|
22
|
+
return key.replace(/_[\w\d]/g, function (match, index, string) {
|
23
|
+
return index === 0 ? match : string.charAt(index + 1).toUpperCase();
|
24
|
+
});
|
25
|
+
}
|
26
|
+
|
27
|
+
function underscore(key) {
|
28
|
+
if (!angular.isString(key)) {
|
29
|
+
return key;
|
30
|
+
}
|
31
|
+
|
32
|
+
// TODO match the latest logic from Active Support
|
33
|
+
return key.replace(/[A-Z]/g, function (match, index) {
|
34
|
+
return index === 0 ? match : '_' + match.toLowerCase();
|
35
|
+
});
|
36
|
+
}
|
37
|
+
|
38
|
+
function pluralize(value) {
|
39
|
+
// TODO match Active Support
|
40
|
+
return value + 's';
|
41
|
+
}
|
42
|
+
|
43
|
+
return {
|
44
|
+
camelize: camelize,
|
45
|
+
underscore: underscore,
|
46
|
+
pluralize: pluralize
|
47
|
+
}
|
48
|
+
});
|
49
|
+
}());
|
50
|
+
(function (undefined) {
|
51
|
+
angular.module('rails').factory('RailsResourceInjector', ['$injector', function($injector) {
|
52
|
+
/**
|
53
|
+
* Allow dependencies to be referenced by name or instance. If referenced by name AngularJS $injector
|
54
|
+
* is used to retrieve the dependency.
|
55
|
+
*
|
56
|
+
* @param dependency (string | function) The dependency to retrieve
|
57
|
+
* @returns {*} The dependency
|
58
|
+
*/
|
59
|
+
function getDependency(dependency) {
|
60
|
+
if (dependency) {
|
61
|
+
return angular.isString(dependency) ? $injector.get(dependency) : dependency
|
62
|
+
}
|
63
|
+
|
64
|
+
return undefined;
|
65
|
+
}
|
66
|
+
|
67
|
+
/**
|
68
|
+
* Looks up and instantiates an instance of the requested service. If the service is not a string then it is
|
69
|
+
* assumed to be a constuctor function.
|
70
|
+
*
|
71
|
+
* @param service (string | function) The service to instantiate
|
72
|
+
* @returns {*} A new instance of the requested service
|
73
|
+
*/
|
74
|
+
function createService(service) {
|
75
|
+
if (service) {
|
76
|
+
return $injector.instantiate(getDependency(service));
|
77
|
+
}
|
78
|
+
|
79
|
+
return undefined;
|
80
|
+
}
|
81
|
+
|
82
|
+
return {
|
83
|
+
createService: createService,
|
84
|
+
getDependency: getDependency
|
85
|
+
}
|
86
|
+
}]);
|
87
|
+
}());
|
88
|
+
/**
|
89
|
+
* @ngdoc function
|
90
|
+
* @name rails.railsUrlBuilder
|
91
|
+
* @function
|
92
|
+
* @requires $interpolate
|
93
|
+
*
|
94
|
+
* @description
|
95
|
+
*
|
96
|
+
* Compiles a URL template string into an interpolation function using $interpolate. If no interpolation bindings
|
97
|
+
* found then {{id}} is appended to the url string.
|
98
|
+
*
|
99
|
+
<pre>
|
100
|
+
expect(railsUrlBuilder('/books')()).toEqual('/books')
|
101
|
+
expect(railsUrlBuilder('/books')({id: 1})).toEqual('/books/1')
|
102
|
+
expect(railsUrlBuilder('/authors/{{authorId}}/books/{{id}}')({id: 1, authorId: 2})).toEqual('/authors/2/books/1')
|
103
|
+
</pre>
|
104
|
+
*
|
105
|
+
* If the $interpolate startSymbol and endSymbol have been customized those values should be used instead of {{ and }}
|
106
|
+
*
|
107
|
+
* @param {string|function} url If the url is a function then that function is returned. Otherwise the url string
|
108
|
+
* is passed to $interpolate as an expression.
|
109
|
+
*
|
110
|
+
* @returns {function(context)} As stated by $interpolate documentation:
|
111
|
+
* An interpolation function which is used to compute the interpolated
|
112
|
+
* string. The function has these parameters:
|
113
|
+
*
|
114
|
+
* * `context`: an object against which any expressions embedded in the strings are evaluated
|
115
|
+
* against.
|
116
|
+
*
|
117
|
+
*/
|
118
|
+
(function (undefined) {
|
119
|
+
angular.module('rails').factory('railsUrlBuilder', ['$interpolate', function($interpolate) {
|
120
|
+
return function (url) {
|
121
|
+
var expression;
|
122
|
+
|
123
|
+
if (angular.isFunction(url)) {
|
124
|
+
return url;
|
125
|
+
}
|
126
|
+
|
127
|
+
if (url.indexOf($interpolate.startSymbol()) === -1) {
|
128
|
+
url = url + '/' + $interpolate.startSymbol() + 'id' + $interpolate.endSymbol();
|
129
|
+
}
|
130
|
+
|
131
|
+
expression = $interpolate(url);
|
132
|
+
|
133
|
+
return function (params) {
|
134
|
+
url = expression(params);
|
135
|
+
|
136
|
+
if (url.charAt(url.length - 1) === '/') {
|
137
|
+
url = url.substr(0, url.length - 1);
|
138
|
+
}
|
139
|
+
|
140
|
+
return url;
|
141
|
+
};
|
142
|
+
};
|
143
|
+
|
144
|
+
}])
|
145
|
+
}());
|
146
|
+
(function (undefined) {
|
147
|
+
angular.module('rails').provider('railsSerializer', function() {
|
148
|
+
var defaultOptions = {
|
149
|
+
underscore: undefined,
|
150
|
+
camelize: undefined,
|
151
|
+
pluralize: undefined,
|
152
|
+
exclusionMatchers: []
|
153
|
+
};
|
154
|
+
|
155
|
+
/**
|
156
|
+
* Configures the underscore method used by the serializer. If not defined then <code>RailsInflector.underscore</code>
|
157
|
+
* will be used.
|
158
|
+
*
|
159
|
+
* @param {function(string):string} fn The function to use for underscore conversion
|
160
|
+
* @returns {railsSerializerProvider} The provider for chaining
|
161
|
+
*/
|
162
|
+
this.underscore = function(fn) {
|
163
|
+
defaultOptions.underscore = fn;
|
164
|
+
return this;
|
165
|
+
};
|
166
|
+
|
167
|
+
/**
|
168
|
+
* Configures the camelize method used by the serializer. If not defined then <code>RailsInflector.camelize</code>
|
169
|
+
* will be used.
|
170
|
+
*
|
171
|
+
* @param {function(string):string} fn The function to use for camelize conversion
|
172
|
+
* @returns {railsSerializerProvider} The provider for chaining
|
173
|
+
*/
|
174
|
+
this.camelize = function(fn) {
|
175
|
+
defaultOptions.camelize = fn;
|
176
|
+
return this;
|
177
|
+
};
|
178
|
+
|
179
|
+
/**
|
180
|
+
* Configures the pluralize method used by the serializer. If not defined then <code>RailsInflector.pluralize</code>
|
181
|
+
* will be used.
|
182
|
+
*
|
183
|
+
* @param {function(string):string} fn The function to use for pluralizing strings.
|
184
|
+
* @returns {railsSerializerProvider} The provider for chaining
|
185
|
+
*/
|
186
|
+
this.pluralize = function(fn) {
|
187
|
+
defaultOptions.pluralize = fn;
|
188
|
+
return this;
|
189
|
+
};
|
190
|
+
|
191
|
+
/**
|
192
|
+
* Configures the array exclusion matchers by the serializer. Exclusion matchers can be one of the following:
|
193
|
+
* * string - Defines a prefix that is used to test for exclusion
|
194
|
+
* * RegExp - A custom regular expression that is tested against the attribute name
|
195
|
+
* * function - A custom function that accepts a string argument and returns a boolean with true indicating exclusion.
|
196
|
+
*
|
197
|
+
* @param {Array.<string|function(string):boolean|RegExp} exclusions An array of exclusion matchers
|
198
|
+
* @returns {railsSerializerProvider} The provider for chaining
|
199
|
+
*/
|
200
|
+
this.exclusionMatchers = function(exclusions) {
|
201
|
+
defaultOptions.exclusionMatchers = exclusions;
|
202
|
+
return this;
|
203
|
+
};
|
204
|
+
|
205
|
+
this.$get = ['$injector', 'RailsInflector', 'RailsResourceInjector', function ($injector, RailsInflector, RailsResourceInjector) {
|
206
|
+
defaultOptions.underscore = defaultOptions.underscore || RailsInflector.underscore;
|
207
|
+
defaultOptions.camelize = defaultOptions.camelize || RailsInflector.camelize;
|
208
|
+
defaultOptions.pluralize = defaultOptions.pluralize || RailsInflector.pluralize;
|
209
|
+
|
210
|
+
function railsSerializer(options, customizer) {
|
211
|
+
|
212
|
+
function Serializer() {
|
213
|
+
if (angular.isFunction(options)) {
|
214
|
+
customizer = options;
|
215
|
+
options = {};
|
216
|
+
}
|
217
|
+
|
218
|
+
this.exclusions = {};
|
219
|
+
this.inclusions = {};
|
220
|
+
this.serializeMappings = {};
|
221
|
+
this.deserializeMappings = {};
|
222
|
+
this.customSerializedAttributes = {};
|
223
|
+
this.preservedAttributes = {};
|
224
|
+
this.customSerializers = {};
|
225
|
+
this.nestedResources = {};
|
226
|
+
this.options = angular.extend({excludeByDefault: false}, defaultOptions, options || {});
|
227
|
+
|
228
|
+
if (customizer) {
|
229
|
+
customizer.call(this, this);
|
230
|
+
}
|
231
|
+
}
|
232
|
+
|
233
|
+
/**
|
234
|
+
* Accepts a variable list of attribute names to exclude from JSON serialization.
|
235
|
+
*
|
236
|
+
* @param attributeNames... {string} Variable number of attribute name parameters
|
237
|
+
* @returns {Serializer} this for chaining support
|
238
|
+
*/
|
239
|
+
Serializer.prototype.exclude = function () {
|
240
|
+
var exclusions = this.exclusions;
|
241
|
+
|
242
|
+
angular.forEach(arguments, function (attributeName) {
|
243
|
+
exclusions[attributeName] = false;
|
244
|
+
});
|
245
|
+
|
246
|
+
return this;
|
247
|
+
};
|
248
|
+
|
249
|
+
/**
|
250
|
+
* Accepts a variable list of attribute names that should be included in JSON serialization.
|
251
|
+
* Using this method will by default exclude all other attributes and only the ones explicitly included using <code>only</code> will be serialized.
|
252
|
+
* @param attributeNames... {string} Variable number of attribute name parameters
|
253
|
+
* @returns {Serializer} this for chaining support
|
254
|
+
*/
|
255
|
+
Serializer.prototype.only = function () {
|
256
|
+
var inclusions = this.inclusions;
|
257
|
+
this.options.excludeByDefault = true;
|
258
|
+
|
259
|
+
angular.forEach(arguments, function (attributeName) {
|
260
|
+
inclusions[attributeName] = true;
|
261
|
+
});
|
262
|
+
|
263
|
+
return this;
|
264
|
+
};
|
265
|
+
|
266
|
+
/**
|
267
|
+
* This is a shortcut for rename that allows you to specify a variable number of attributes that should all be renamed to
|
268
|
+
* <code>{attributeName}_attributes</code> to work with the Rails nested_attributes feature.
|
269
|
+
* @param attributeNames... {string} Variable number of attribute name parameters
|
270
|
+
* @returns {Serializer} this for chaining support
|
271
|
+
*/
|
272
|
+
Serializer.prototype.nestedAttribute = function () {
|
273
|
+
var self = this;
|
274
|
+
|
275
|
+
angular.forEach(arguments, function (attributeName) {
|
276
|
+
self.rename(attributeName, attributeName + '_attributes');
|
277
|
+
});
|
278
|
+
|
279
|
+
return this;
|
280
|
+
};
|
281
|
+
|
282
|
+
/**
|
283
|
+
* Specifies an attribute that is a nested resource within the parent object.
|
284
|
+
* Nested resources do not imply nested attributes, if you want both you still have to specify call <code>nestedAttribute</code> as well.
|
285
|
+
*
|
286
|
+
* A nested resource serves two purposes. First, it defines the resource that should be used when constructing resources from the server.
|
287
|
+
* Second, it specifies how the nested object should be serialized.
|
288
|
+
*
|
289
|
+
* An optional third parameter <code>serializer</code> is available to override the serialization logic
|
290
|
+
* of the resource in case you need to serialize it differently in multiple contexts.
|
291
|
+
*
|
292
|
+
* @param attributeName {string} The name of the attribute that is a nested resource
|
293
|
+
* @param resource {string | Resource} A reference to the resource that the attribute is a type of.
|
294
|
+
* @param serializer {string | Serializer} (optional) An optional serializer reference to override the nested resource's default serializer
|
295
|
+
* @returns {Serializer} this for chaining support
|
296
|
+
*/
|
297
|
+
Serializer.prototype.resource = function (attributeName, resource, serializer) {
|
298
|
+
this.nestedResources[attributeName] = resource;
|
299
|
+
|
300
|
+
if (serializer) {
|
301
|
+
this.serializeWith(attributeName, serializer);
|
302
|
+
}
|
303
|
+
|
304
|
+
return this;
|
305
|
+
};
|
306
|
+
|
307
|
+
/**
|
308
|
+
* Specifies a custom name mapping for an attribute.
|
309
|
+
* On serializing to JSON the jsonName will be used.
|
310
|
+
* On deserialization, if jsonName is seen then it will be renamed as javascriptName in the resulting resource.
|
311
|
+
*
|
312
|
+
* @param javascriptName {string} The attribute name as it appears in the JavaScript object
|
313
|
+
* @param jsonName {string} The attribute name as it should appear in JSON
|
314
|
+
* @param bidirectional {boolean} (optional) Allows turning off the bidirectional renaming, defaults to true.
|
315
|
+
* @returns {Serializer} this for chaining support
|
316
|
+
*/
|
317
|
+
Serializer.prototype.rename = function (javascriptName, jsonName, bidirectional) {
|
318
|
+
this.serializeMappings[javascriptName] = jsonName;
|
319
|
+
|
320
|
+
if (bidirectional || bidirectional === undefined) {
|
321
|
+
this.deserializeMappings[jsonName] = javascriptName;
|
322
|
+
}
|
323
|
+
return this;
|
324
|
+
};
|
325
|
+
|
326
|
+
/**
|
327
|
+
* Allows custom attribute creation as part of the serialization to JSON.
|
328
|
+
*
|
329
|
+
* @param attributeName {string} The name of the attribute to add
|
330
|
+
* @param value {*} The value to add, if specified as a function then the function will be called during serialization
|
331
|
+
* and should return the value to add.
|
332
|
+
* @returns {Serializer} this for chaining support
|
333
|
+
*/
|
334
|
+
Serializer.prototype.add = function (attributeName, value) {
|
335
|
+
this.customSerializedAttributes[attributeName] = value;
|
336
|
+
return this;
|
337
|
+
};
|
338
|
+
|
339
|
+
|
340
|
+
/**
|
341
|
+
* Allows the attribute to be preserved unmodified in the resulting object.
|
342
|
+
*
|
343
|
+
* @param attributeName {string} The name of the attribute to add
|
344
|
+
* @returns {Serializer} this for chaining support
|
345
|
+
*/
|
346
|
+
Serializer.prototype.preserve = function(attributeName) {
|
347
|
+
this.preservedAttributes[attributeName] = true;
|
348
|
+
return this;
|
349
|
+
};
|
350
|
+
|
351
|
+
/**
|
352
|
+
* Specify a custom serializer to use for an attribute.
|
353
|
+
*
|
354
|
+
* @param attributeName {string} The name of the attribute
|
355
|
+
* @param serializer {string | constructor} A reference to the custom serializer to use for the attribute.
|
356
|
+
* @returns {Serializer} this for chaining support
|
357
|
+
*/
|
358
|
+
Serializer.prototype.serializeWith = function (attributeName, serializer) {
|
359
|
+
this.customSerializers[attributeName] = serializer;
|
360
|
+
return this;
|
361
|
+
};
|
362
|
+
|
363
|
+
/**
|
364
|
+
* Determines whether or not an attribute should be excluded.
|
365
|
+
*
|
366
|
+
* If the option excludeByDefault has been set then attributes will default to excluded and will only
|
367
|
+
* be included if they have been included using the "only" customization function.
|
368
|
+
*
|
369
|
+
* If the option excludeByDefault has not been set then attributes must be explicitly excluded using the "exclude"
|
370
|
+
* customization function or must be matched by one of the exclusionMatchers.
|
371
|
+
*
|
372
|
+
* @param attributeName The name of the attribute to check for exclusion
|
373
|
+
* @returns {boolean} true if excluded, false otherwise
|
374
|
+
*/
|
375
|
+
Serializer.prototype.isExcludedFromSerialization = function (attributeName) {
|
376
|
+
if ((this.options.excludeByDefault && !this.inclusions.hasOwnProperty(attributeName)) || this.exclusions.hasOwnProperty(attributeName)) {
|
377
|
+
return true;
|
378
|
+
}
|
379
|
+
|
380
|
+
if (this.options.exclusionMatchers) {
|
381
|
+
var excluded = false;
|
382
|
+
|
383
|
+
angular.forEach(this.options.exclusionMatchers, function (matcher) {
|
384
|
+
if (angular.isString(matcher)) {
|
385
|
+
excluded = excluded || attributeName.indexOf(matcher) === 0;
|
386
|
+
} else if (angular.isFunction(matcher)) {
|
387
|
+
excluded = excluded || matcher.call(undefined, attributeName);
|
388
|
+
} else if (matcher instanceof RegExp) {
|
389
|
+
excluded = excluded || matcher.test(attributeName);
|
390
|
+
}
|
391
|
+
});
|
392
|
+
|
393
|
+
return excluded;
|
394
|
+
}
|
395
|
+
|
396
|
+
return false;
|
397
|
+
};
|
398
|
+
|
399
|
+
/**
|
400
|
+
* Remaps the attribute name to the serialized form which includes:
|
401
|
+
* - checking for exclusion
|
402
|
+
* - remapping to a custom value specified by the rename customization function
|
403
|
+
* - underscoring the name
|
404
|
+
*
|
405
|
+
* @param attributeName The current attribute name
|
406
|
+
* @returns {*} undefined if the attribute should be excluded or the mapped attribute name
|
407
|
+
*/
|
408
|
+
Serializer.prototype.getSerializedAttributeName = function (attributeName) {
|
409
|
+
var mappedName = this.serializeMappings[attributeName] || attributeName;
|
410
|
+
|
411
|
+
if (this.isExcludedFromSerialization(attributeName) || this.isExcludedFromSerialization(mappedName)) {
|
412
|
+
return undefined;
|
413
|
+
}
|
414
|
+
|
415
|
+
return this.underscore(mappedName);
|
416
|
+
};
|
417
|
+
|
418
|
+
/**
|
419
|
+
* Determines whether or not an attribute should be excluded from deserialization.
|
420
|
+
*
|
421
|
+
* By default, we do not exclude any attributes from deserialization.
|
422
|
+
*
|
423
|
+
* @param attributeName The name of the attribute to check for exclusion
|
424
|
+
* @returns {boolean} true if excluded, false otherwise
|
425
|
+
*/
|
426
|
+
Serializer.prototype.isExcludedFromDeserialization = function (attributeName) {
|
427
|
+
return false;
|
428
|
+
};
|
429
|
+
|
430
|
+
/**
|
431
|
+
* Remaps the attribute name to the deserialized form which includes:
|
432
|
+
* - camelizing the name
|
433
|
+
* - checking for exclusion
|
434
|
+
* - remapping to a custom value specified by the rename customization function
|
435
|
+
*
|
436
|
+
* @param attributeName The current attribute name
|
437
|
+
* @returns {*} undefined if the attribute should be excluded or the mapped attribute name
|
438
|
+
*/
|
439
|
+
Serializer.prototype.getDeserializedAttributeName = function (attributeName) {
|
440
|
+
var camelizedName = this.camelize(attributeName);
|
441
|
+
|
442
|
+
camelizedName = this.deserializeMappings[attributeName]
|
443
|
+
|| this.deserializeMappings[camelizedName]
|
444
|
+
|| camelizedName;
|
445
|
+
|
446
|
+
if (this.isExcludedFromDeserialization(attributeName) || this.isExcludedFromDeserialization(camelizedName)) {
|
447
|
+
return undefined;
|
448
|
+
}
|
449
|
+
|
450
|
+
return camelizedName;
|
451
|
+
};
|
452
|
+
|
453
|
+
/**
|
454
|
+
* Returns a reference to the nested resource that has been specified for the attribute.
|
455
|
+
* @param attributeName The attribute name
|
456
|
+
* @returns {*} undefined if no nested resource has been specified or a reference to the nested resource class
|
457
|
+
*/
|
458
|
+
Serializer.prototype.getNestedResource = function (attributeName) {
|
459
|
+
return RailsResourceInjector.getDependency(this.nestedResources[attributeName]);
|
460
|
+
};
|
461
|
+
|
462
|
+
/**
|
463
|
+
* Returns a custom serializer for the attribute if one has been specified. Custom serializers can be specified
|
464
|
+
* in one of two ways. The serializeWith customization method allows specifying a custom serializer for any attribute.
|
465
|
+
* Or an attribute could have been specified as a nested resource in which case the nested resource's serializer
|
466
|
+
* is used. Custom serializers specified using serializeWith take precedence over the nested resource serializer.
|
467
|
+
*
|
468
|
+
* @param attributeName The attribute name
|
469
|
+
* @returns {*} undefined if no custom serializer has been specified or an instance of the Serializer
|
470
|
+
*/
|
471
|
+
Serializer.prototype.getAttributeSerializer = function (attributeName) {
|
472
|
+
var resource = this.getNestedResource(attributeName),
|
473
|
+
serializer = this.customSerializers[attributeName];
|
474
|
+
|
475
|
+
// custom serializer takes precedence over resource serializer
|
476
|
+
if (serializer) {
|
477
|
+
return RailsResourceInjector.createService(serializer)
|
478
|
+
} else if (resource) {
|
479
|
+
return resource.serializer;
|
480
|
+
}
|
481
|
+
|
482
|
+
return undefined;
|
483
|
+
};
|
484
|
+
|
485
|
+
|
486
|
+
/**
|
487
|
+
* Prepares the data for serialization to JSON.
|
488
|
+
*
|
489
|
+
* @param data The data to prepare
|
490
|
+
* @returns {*} A new object or array that is ready for JSON serialization
|
491
|
+
*/
|
492
|
+
Serializer.prototype.serializeValue = function (data) {
|
493
|
+
var result = data,
|
494
|
+
self = this;
|
495
|
+
|
496
|
+
if (angular.isArray(data)) {
|
497
|
+
result = [];
|
498
|
+
|
499
|
+
angular.forEach(data, function (value) {
|
500
|
+
result.push(self.serializeValue(value));
|
501
|
+
});
|
502
|
+
} else if (angular.isObject(data)) {
|
503
|
+
if (angular.isDate(data)) {
|
504
|
+
return data;
|
505
|
+
}
|
506
|
+
result = {};
|
507
|
+
|
508
|
+
angular.forEach(data, function (value, key) {
|
509
|
+
// if the value is a function then it can't be serialized to JSON so we'll just skip it
|
510
|
+
if (!angular.isFunction(value)) {
|
511
|
+
self.serializeAttribute(result, key, value);
|
512
|
+
}
|
513
|
+
});
|
514
|
+
}
|
515
|
+
|
516
|
+
return result;
|
517
|
+
};
|
518
|
+
|
519
|
+
/**
|
520
|
+
* Transforms an attribute and its value and stores it on the parent data object. The attribute will be
|
521
|
+
* renamed as needed and the value itself will be serialized as well.
|
522
|
+
*
|
523
|
+
* @param data The object that the attribute will be added to
|
524
|
+
* @param attribute The attribute to transform
|
525
|
+
* @param value The current value of the attribute
|
526
|
+
*/
|
527
|
+
Serializer.prototype.serializeAttribute = function (data, attribute, value) {
|
528
|
+
var serializer = this.getAttributeSerializer(attribute),
|
529
|
+
serializedAttributeName = this.getSerializedAttributeName(attribute);
|
530
|
+
|
531
|
+
// undefined means the attribute should be excluded from serialization
|
532
|
+
if (serializedAttributeName === undefined) {
|
533
|
+
return;
|
534
|
+
}
|
535
|
+
|
536
|
+
data[serializedAttributeName] = serializer ? serializer.serialize(value) : this.serializeValue(value);
|
537
|
+
};
|
538
|
+
|
539
|
+
/**
|
540
|
+
* Serializes the data by applying various transformations such as:
|
541
|
+
* - Underscoring attribute names
|
542
|
+
* - attribute renaming
|
543
|
+
* - attribute exclusion
|
544
|
+
* - custom attribute addition
|
545
|
+
*
|
546
|
+
* @param data The data to prepare
|
547
|
+
* @returns {*} A new object or array that is ready for JSON serialization
|
548
|
+
*/
|
549
|
+
Serializer.prototype.serialize = function (data) {
|
550
|
+
var result = this.serializeValue(data),
|
551
|
+
self = this;
|
552
|
+
|
553
|
+
if (angular.isObject(result)) {
|
554
|
+
angular.forEach(this.customSerializedAttributes, function (value, key) {
|
555
|
+
if (angular.isFunction(value)) {
|
556
|
+
value = value.call(data, data);
|
557
|
+
}
|
558
|
+
|
559
|
+
self.serializeAttribute(result, key, value);
|
560
|
+
});
|
561
|
+
}
|
562
|
+
|
563
|
+
return result;
|
564
|
+
};
|
565
|
+
|
566
|
+
/**
|
567
|
+
* Iterates over the data deserializing each entry on arrays and each key/value on objects.
|
568
|
+
*
|
569
|
+
* @param data The object to deserialize
|
570
|
+
* @param Resource (optional) The resource type to deserialize the result into
|
571
|
+
* @returns {*} A new object or an instance of Resource populated with deserialized data.
|
572
|
+
*/
|
573
|
+
Serializer.prototype.deserializeValue = function (data, Resource) {
|
574
|
+
var result = data,
|
575
|
+
self = this;
|
576
|
+
|
577
|
+
if (angular.isArray(data)) {
|
578
|
+
result = [];
|
579
|
+
|
580
|
+
angular.forEach(data, function (value) {
|
581
|
+
result.push(self.deserializeValue(value, Resource));
|
582
|
+
});
|
583
|
+
} else if (angular.isObject(data)) {
|
584
|
+
if (angular.isDate(data)) {
|
585
|
+
return data;
|
586
|
+
}
|
587
|
+
|
588
|
+
result = {};
|
589
|
+
|
590
|
+
if (Resource) {
|
591
|
+
result = new Resource();
|
592
|
+
}
|
593
|
+
|
594
|
+
angular.forEach(data, function (value, key) {
|
595
|
+
self.deserializeAttribute(result, key, value);
|
596
|
+
});
|
597
|
+
}
|
598
|
+
|
599
|
+
return result;
|
600
|
+
};
|
601
|
+
|
602
|
+
/**
|
603
|
+
* Transforms an attribute and its value and stores it on the parent data object. The attribute will be
|
604
|
+
* renamed as needed and the value itself will be deserialized as well.
|
605
|
+
*
|
606
|
+
* @param data The object that the attribute will be added to
|
607
|
+
* @param attribute The attribute to transform
|
608
|
+
* @param value The current value of the attribute
|
609
|
+
*/
|
610
|
+
Serializer.prototype.deserializeAttribute = function (data, attribute, value) {
|
611
|
+
var serializer,
|
612
|
+
NestedResource,
|
613
|
+
attributeName = this.getDeserializedAttributeName(attribute);
|
614
|
+
|
615
|
+
// undefined means the attribute should be excluded from serialization
|
616
|
+
if (attributeName === undefined) {
|
617
|
+
return;
|
618
|
+
}
|
619
|
+
|
620
|
+
serializer = this.getAttributeSerializer(attributeName);
|
621
|
+
NestedResource = this.getNestedResource(attributeName);
|
622
|
+
|
623
|
+
// preserved attributes are assigned unmodified
|
624
|
+
if (this.preservedAttributes[attributeName]) {
|
625
|
+
data[attributeName] = value;
|
626
|
+
} else {
|
627
|
+
data[attributeName] = serializer ? serializer.deserialize(value, NestedResource) : this.deserializeValue(value, NestedResource);
|
628
|
+
}
|
629
|
+
};
|
630
|
+
|
631
|
+
/**
|
632
|
+
* Deserializes the data by applying various transformations such as:
|
633
|
+
* - Camelizing attribute names
|
634
|
+
* - attribute renaming
|
635
|
+
* - attribute exclusion
|
636
|
+
* - nested resource creation
|
637
|
+
*
|
638
|
+
* @param data The object to deserialize
|
639
|
+
* @param Resource (optional) The resource type to deserialize the result into
|
640
|
+
* @returns {*} A new object or an instance of Resource populated with deserialized data
|
641
|
+
*/
|
642
|
+
Serializer.prototype.deserialize = function (data, Resource) {
|
643
|
+
// just calls deserializeValue for now so we can more easily add on custom attribute logic for deserialize too
|
644
|
+
return this.deserializeValue(data, Resource);
|
645
|
+
};
|
646
|
+
|
647
|
+
Serializer.prototype.pluralize = function (value) {
|
648
|
+
if (this.options.pluralize) {
|
649
|
+
return this.options.pluralize(value);
|
650
|
+
}
|
651
|
+
return value;
|
652
|
+
};
|
653
|
+
|
654
|
+
Serializer.prototype.underscore = function (value) {
|
655
|
+
if (this.options.underscore) {
|
656
|
+
return this.options.underscore(value);
|
657
|
+
}
|
658
|
+
return value;
|
659
|
+
};
|
660
|
+
|
661
|
+
Serializer.prototype.camelize = function (value) {
|
662
|
+
if (this.options.camelize) {
|
663
|
+
return this.options.camelize(value);
|
664
|
+
}
|
665
|
+
return value;
|
666
|
+
}
|
667
|
+
|
668
|
+
return Serializer;
|
669
|
+
}
|
670
|
+
|
671
|
+
railsSerializer.defaultOptions = defaultOptions;
|
672
|
+
return railsSerializer;
|
673
|
+
}];
|
674
|
+
});
|
675
|
+
}());
|
676
|
+
|
677
|
+
(function (undefined) {
|
678
|
+
angular.module('rails').factory('railsRootWrappingTransformer', function () {
|
679
|
+
return function (data, resource) {
|
680
|
+
var result = {};
|
681
|
+
result[angular.isArray(data) ? resource.rootPluralName : resource.rootName] = data;
|
682
|
+
return result;
|
683
|
+
};
|
684
|
+
});
|
685
|
+
|
686
|
+
angular.module('rails').factory('railsRootWrappingInterceptor', function () {
|
687
|
+
return function (promise) {
|
688
|
+
var resource = promise.resource;
|
689
|
+
|
690
|
+
if (!resource) {
|
691
|
+
return promise;
|
692
|
+
}
|
693
|
+
|
694
|
+
return promise.then(function (response) {
|
695
|
+
if (response.data && response.data.hasOwnProperty(resource.rootName)) {
|
696
|
+
response.data = response.data[resource.rootName];
|
697
|
+
} else if (response.data && response.data.hasOwnProperty(resource.rootPluralName)) {
|
698
|
+
response.data = response.data[resource.rootPluralName];
|
699
|
+
}
|
700
|
+
|
701
|
+
return response;
|
702
|
+
});
|
703
|
+
};
|
704
|
+
});
|
705
|
+
|
706
|
+
angular.module('rails').provider('railsResourceFactory', function () {
|
707
|
+
var defaultOptions = {
|
708
|
+
enableRootWrapping: true,
|
709
|
+
updateMethod: 'put',
|
710
|
+
httpConfig: {},
|
711
|
+
defaultParams: undefined
|
712
|
+
};
|
713
|
+
|
714
|
+
this.enableRootWrapping = function (value) {
|
715
|
+
defaultOptions.enableRootWrapping = value;
|
716
|
+
return this;
|
717
|
+
};
|
718
|
+
|
719
|
+
this.updateMethod = function (value) {
|
720
|
+
defaultOptions.updateMethod = value;
|
721
|
+
return this;
|
722
|
+
};
|
723
|
+
|
724
|
+
this.httpConfig = function (value) {
|
725
|
+
defaultOptions.httpConfig = value;
|
726
|
+
return this;
|
727
|
+
};
|
728
|
+
|
729
|
+
this.defaultParams = function (value) {
|
730
|
+
defaultOptions.defaultParams = value;
|
731
|
+
return this;
|
732
|
+
};
|
733
|
+
|
734
|
+
this.$get = ['$http', '$q', 'railsUrlBuilder', 'railsSerializer', 'railsRootWrappingTransformer', 'railsRootWrappingInterceptor', 'RailsResourceInjector',
|
735
|
+
function ($http, $q, railsUrlBuilder, railsSerializer, railsRootWrappingTransformer, railsRootWrappingInterceptor, RailsResourceInjector) {
|
736
|
+
|
737
|
+
function railsResourceFactory(config) {
|
738
|
+
var transformers = config.requestTransformers,
|
739
|
+
interceptors = config.responseInterceptors,
|
740
|
+
afterInterceptors = config.afterResponseInterceptors;
|
741
|
+
|
742
|
+
function appendPath(url, path) {
|
743
|
+
if (path) {
|
744
|
+
if (path[0] !== '/') {
|
745
|
+
url += '/';
|
746
|
+
}
|
747
|
+
|
748
|
+
url += path;
|
749
|
+
}
|
750
|
+
|
751
|
+
return url;
|
752
|
+
}
|
753
|
+
|
754
|
+
function RailsResource(value) {
|
755
|
+
var instance = this;
|
756
|
+
if (value) {
|
757
|
+
var immediatePromise = function(data) {
|
758
|
+
return {
|
759
|
+
resource: RailsResource,
|
760
|
+
context: instance,
|
761
|
+
response: data,
|
762
|
+
then: function(callback) {
|
763
|
+
this.response = callback(this.response, this.resource, this.context);
|
764
|
+
return immediatePromise(this.response);
|
765
|
+
}
|
766
|
+
}
|
767
|
+
};
|
768
|
+
|
769
|
+
var data = RailsResource.callInterceptors(immediatePromise({data: value}), this).response.data;
|
770
|
+
angular.extend(this, data);
|
771
|
+
}
|
772
|
+
}
|
773
|
+
|
774
|
+
RailsResource.setUrl = function(url) {
|
775
|
+
RailsResource.url = railsUrlBuilder(url);
|
776
|
+
};
|
777
|
+
RailsResource.setUrl(config.url);
|
778
|
+
|
779
|
+
RailsResource.enableRootWrapping = config.wrapData === undefined ? defaultOptions.enableRootWrapping : config.wrapData; // using undefined check because config.wrapData || true would be true when config.wrapData === false
|
780
|
+
RailsResource.httpConfig = config.httpConfig || defaultOptions.httpConfig;
|
781
|
+
RailsResource.httpConfig.headers = angular.extend({'Accept': 'application/json', 'Content-Type': 'application/json'}, RailsResource.httpConfig.headers || {});
|
782
|
+
RailsResource.defaultParams = config.defaultParams || defaultOptions.defaultParams;
|
783
|
+
RailsResource.updateMethod = (config.updateMethod || defaultOptions.updateMethod).toLowerCase();
|
784
|
+
|
785
|
+
RailsResource.requestTransformers = [];
|
786
|
+
RailsResource.responseInterceptors = [];
|
787
|
+
RailsResource.afterResponseInterceptors = [];
|
788
|
+
RailsResource.serializer = RailsResourceInjector.createService(config.serializer || railsSerializer());
|
789
|
+
RailsResource.rootName = RailsResource.serializer.underscore(config.name);
|
790
|
+
RailsResource.rootPluralName = RailsResource.serializer.underscore(config.pluralName || RailsResource.serializer.pluralize(config.name));
|
791
|
+
|
792
|
+
/**
|
793
|
+
* Add a callback to run on response and construction.
|
794
|
+
* @param fn(response data, constructor, context) - response data is either the resource instance returned or an array of resource instances,
|
795
|
+
* constructor is the resource class calling the function,
|
796
|
+
* context is the resource instance of the calling method (create, update, delete) or undefined if the method was a class method (get, query)
|
797
|
+
*/
|
798
|
+
RailsResource.beforeResponse = function(fn) {
|
799
|
+
fn = RailsResourceInjector.getDependency(fn);
|
800
|
+
RailsResource.responseInterceptors.push(function(promise) {
|
801
|
+
return promise.then(function(response) {
|
802
|
+
fn(response.data, promise.resource, promise.context);
|
803
|
+
return response;
|
804
|
+
});
|
805
|
+
});
|
806
|
+
};
|
807
|
+
|
808
|
+
/**
|
809
|
+
* Add a callback to run after response has been processed. These callbacks are not called on object construction.
|
810
|
+
* @param fn(response data, constructor) - response data is either the resource instance returned or an array of resource instances and constructor is the resource class calling the function
|
811
|
+
*/
|
812
|
+
RailsResource.afterResponse = function(fn) {
|
813
|
+
fn = RailsResourceInjector.getDependency(fn);
|
814
|
+
RailsResource.afterResponseInterceptors.push(function(promise) {
|
815
|
+
return promise.then(function(response) {
|
816
|
+
fn(response, promise.resource);
|
817
|
+
return response;
|
818
|
+
});
|
819
|
+
});
|
820
|
+
};
|
821
|
+
|
822
|
+
/**
|
823
|
+
* Adds a function to run after serializing the data to send to the server, but before root-wrapping it.
|
824
|
+
* @param fn (data, constructor) - data object is the serialized resource instance, and constructor the resource class calling the function
|
825
|
+
*/
|
826
|
+
RailsResource.beforeRequest = function(fn) {
|
827
|
+
fn = RailsResourceInjector.getDependency(fn);
|
828
|
+
RailsResource.requestTransformers.push(function(data, resource) {
|
829
|
+
return fn(data, resource) || data;
|
830
|
+
});
|
831
|
+
};
|
832
|
+
|
833
|
+
// copied from $HttpProvider to support interceptors being dependency names or anonymous factory functions
|
834
|
+
angular.forEach(interceptors, function (interceptor) {
|
835
|
+
RailsResource.responseInterceptors.push(RailsResourceInjector.getDependency(interceptor));
|
836
|
+
});
|
837
|
+
|
838
|
+
angular.forEach(afterInterceptors, function (interceptor) {
|
839
|
+
RailsResource.afterResponseInterceptors.push(RailsResourceInjector.getDependency(interceptor));
|
840
|
+
});
|
841
|
+
|
842
|
+
angular.forEach(transformers, function (transformer) {
|
843
|
+
RailsResource.requestTransformers.push(RailsResourceInjector.getDependency(transformer));
|
844
|
+
});
|
845
|
+
|
846
|
+
// transform data for request:
|
847
|
+
RailsResource.transformData = function (data) {
|
848
|
+
data = RailsResource.serializer.serialize(data);
|
849
|
+
|
850
|
+
// data is now serialized. call request transformers including beforeRequest
|
851
|
+
angular.forEach(RailsResource.requestTransformers, function (transformer) {
|
852
|
+
data = transformer(data, RailsResource);
|
853
|
+
});
|
854
|
+
|
855
|
+
|
856
|
+
if (RailsResource.enableRootWrapping) {
|
857
|
+
data = railsRootWrappingTransformer(data, RailsResource);
|
858
|
+
}
|
859
|
+
|
860
|
+
return data;
|
861
|
+
};
|
862
|
+
|
863
|
+
// transform data on response:
|
864
|
+
RailsResource.callInterceptors = function (promise, context) {
|
865
|
+
promise = promise.then(function (response) {
|
866
|
+
// store off the data in case something (like our root unwrapping) assigns data as a new object
|
867
|
+
response.originalData = response.data;
|
868
|
+
return response;
|
869
|
+
});
|
870
|
+
|
871
|
+
if (RailsResource.enableRootWrapping) {
|
872
|
+
promise.resource = RailsResource;
|
873
|
+
promise = railsRootWrappingInterceptor(promise);
|
874
|
+
}
|
875
|
+
|
876
|
+
promise.then(function (response) {
|
877
|
+
response.data = RailsResource.serializer.deserialize(response.data, RailsResource);
|
878
|
+
return response;
|
879
|
+
});
|
880
|
+
|
881
|
+
// data is now deserialized. call response interceptors including beforeResponse
|
882
|
+
angular.forEach(RailsResource.responseInterceptors, function (interceptor) {
|
883
|
+
promise.resource = RailsResource;
|
884
|
+
promise.context = context;
|
885
|
+
promise = interceptor(promise);
|
886
|
+
});
|
887
|
+
|
888
|
+
return promise;
|
889
|
+
};
|
890
|
+
|
891
|
+
// transform data after response has been converted to a resource instance:
|
892
|
+
RailsResource.callAfterInterceptors = function (promise) {
|
893
|
+
// data is now deserialized. call response interceptors including afterResponse
|
894
|
+
angular.forEach(RailsResource.afterResponseInterceptors, function (interceptor) {
|
895
|
+
promise.resource = RailsResource;
|
896
|
+
promise = interceptor(promise);
|
897
|
+
});
|
898
|
+
|
899
|
+
return promise;
|
900
|
+
};
|
901
|
+
|
902
|
+
RailsResource.processResponse = function (promise) {
|
903
|
+
promise = RailsResource.callInterceptors(promise).then(function (response) {
|
904
|
+
return response.data;
|
905
|
+
});
|
906
|
+
|
907
|
+
return RailsResource.callAfterInterceptors(promise);
|
908
|
+
};
|
909
|
+
|
910
|
+
RailsResource.getParameters = function (queryParams) {
|
911
|
+
var params;
|
912
|
+
|
913
|
+
if (RailsResource.defaultParams) {
|
914
|
+
params = RailsResource.defaultParams;
|
915
|
+
}
|
916
|
+
|
917
|
+
if (angular.isObject(queryParams)) {
|
918
|
+
params = angular.extend(params || {}, queryParams);
|
919
|
+
}
|
920
|
+
|
921
|
+
return params;
|
922
|
+
};
|
923
|
+
|
924
|
+
RailsResource.getHttpConfig = function (queryParams) {
|
925
|
+
var params = RailsResource.getParameters(queryParams);
|
926
|
+
|
927
|
+
if (params) {
|
928
|
+
return angular.extend({params: params}, RailsResource.httpConfig);
|
929
|
+
}
|
930
|
+
|
931
|
+
return angular.copy(RailsResource.httpConfig);
|
932
|
+
};
|
933
|
+
|
934
|
+
/**
|
935
|
+
* Returns a URL from the given parameters. You can override this method on your resource definitions to provide
|
936
|
+
* custom logic for building your URLs or you can utilize the parameterized url strings to substitute values in the
|
937
|
+
* URL string.
|
938
|
+
*
|
939
|
+
* The parameters in the URL string follow the normal Angular binding expression using {{ and }} for the start/end symbols.
|
940
|
+
*
|
941
|
+
* If the context is a number and the URL string does not contain an id parameter then the number is appended
|
942
|
+
* to the URL string.
|
943
|
+
*
|
944
|
+
* If the context is a number and the URL string does
|
945
|
+
* @param context
|
946
|
+
* @param path {string} (optional) An additional path to append to the URL
|
947
|
+
* @return {string}
|
948
|
+
*/
|
949
|
+
RailsResource.$url = RailsResource.resourceUrl = function (context, path) {
|
950
|
+
if (!angular.isObject(context)) {
|
951
|
+
context = {id: context};
|
952
|
+
}
|
953
|
+
|
954
|
+
return appendPath(RailsResource.url(context || {}), path);
|
955
|
+
};
|
956
|
+
|
957
|
+
RailsResource.$get = function (url, queryParams) {
|
958
|
+
return RailsResource.processResponse($http.get(url, RailsResource.getHttpConfig(queryParams)));
|
959
|
+
};
|
960
|
+
|
961
|
+
RailsResource.query = function (queryParams, context) {
|
962
|
+
return RailsResource.$get(RailsResource.resourceUrl(context), queryParams);
|
963
|
+
};
|
964
|
+
|
965
|
+
RailsResource.get = function (context, queryParams) {
|
966
|
+
return RailsResource.$get(RailsResource.resourceUrl(context), queryParams);
|
967
|
+
};
|
968
|
+
|
969
|
+
/**
|
970
|
+
* Returns the URL for this resource.
|
971
|
+
*
|
972
|
+
* @param path {string} (optional) An additional path to append to the URL
|
973
|
+
* @returns {string} The URL for the resource
|
974
|
+
*/
|
975
|
+
RailsResource.prototype.$url = function(path) {
|
976
|
+
return appendPath(RailsResource.resourceUrl(this), path);
|
977
|
+
};
|
978
|
+
|
979
|
+
RailsResource.prototype.processResponse = function (promise) {
|
980
|
+
promise = RailsResource.callInterceptors(promise, this);
|
981
|
+
|
982
|
+
promise = promise.then(angular.bind(this, function (response) {
|
983
|
+
// we may not have response data
|
984
|
+
if (response.hasOwnProperty('data') && angular.isObject(response.data)) {
|
985
|
+
angular.extend(this, response.data);
|
986
|
+
}
|
987
|
+
|
988
|
+
return this;
|
989
|
+
}));
|
990
|
+
|
991
|
+
return RailsResource.callAfterInterceptors(promise);
|
992
|
+
};
|
993
|
+
|
994
|
+
angular.forEach(['post', 'put', 'patch'], function (method) {
|
995
|
+
RailsResource['$' + method] = function (url, data) {
|
996
|
+
var config;
|
997
|
+
// clone so we can manipulate w/o modifying the actual instance
|
998
|
+
data = RailsResource.transformData(angular.copy(data, {}));
|
999
|
+
config = angular.extend({method: method, url: url, data: data}, RailsResource.getHttpConfig());
|
1000
|
+
return RailsResource.processResponse($http(config));
|
1001
|
+
};
|
1002
|
+
|
1003
|
+
RailsResource.prototype['$' + method] = function (url) {
|
1004
|
+
var data, config;
|
1005
|
+
// clone so we can manipulate w/o modifying the actual instance
|
1006
|
+
data = RailsResource.transformData(angular.copy(this, {}));
|
1007
|
+
config = angular.extend({method: method, url: url, data: data}, RailsResource.getHttpConfig());
|
1008
|
+
return this.processResponse($http(config));
|
1009
|
+
|
1010
|
+
};
|
1011
|
+
});
|
1012
|
+
|
1013
|
+
RailsResource.prototype.create = function () {
|
1014
|
+
return this.$post(this.$url(), this);
|
1015
|
+
};
|
1016
|
+
|
1017
|
+
RailsResource.prototype.update = function () {
|
1018
|
+
return this['$' + RailsResource.updateMethod](this.$url(), this);
|
1019
|
+
};
|
1020
|
+
|
1021
|
+
RailsResource.prototype.isNew = function () {
|
1022
|
+
return this.id == null;
|
1023
|
+
}
|
1024
|
+
|
1025
|
+
RailsResource.prototype.save = function () {
|
1026
|
+
if (this.isNew()) {
|
1027
|
+
return this.create();
|
1028
|
+
} else {
|
1029
|
+
return this.update();
|
1030
|
+
}
|
1031
|
+
}
|
1032
|
+
|
1033
|
+
RailsResource['$delete'] = function (url) {
|
1034
|
+
return RailsResource.processResponse($http['delete'](url, RailsResource.getHttpConfig()));
|
1035
|
+
};
|
1036
|
+
|
1037
|
+
RailsResource.prototype['$delete'] = function (url) {
|
1038
|
+
return this.processResponse($http['delete'](url, RailsResource.getHttpConfig()));
|
1039
|
+
};
|
1040
|
+
|
1041
|
+
//using ['delete'] instead of .delete for IE7/8 compatibility
|
1042
|
+
RailsResource.prototype.remove = RailsResource.prototype['delete'] = function () {
|
1043
|
+
return this.$delete(this.$url());
|
1044
|
+
};
|
1045
|
+
|
1046
|
+
return RailsResource;
|
1047
|
+
}
|
1048
|
+
|
1049
|
+
return railsResourceFactory;
|
1050
|
+
}];
|
1051
|
+
});
|
1052
|
+
}());
|