angularjs-rails-resource 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
}());
|