angularjs-rails-resource 0.1.7 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,10 @@
1
+ //= require_self
2
+ //= require_tree ./utils
3
+ //= require ./serialization
4
+ //= require ./resource
5
+
6
+ (function (undefined) {
7
+ angular.module('rails', ['ng']);
8
+ }());
9
+
10
+
@@ -1,61 +1,4 @@
1
1
  (function (undefined) {
2
- angular.module('rails', ['ng']);
3
-
4
- function transformObject(data, transform) {
5
- var newKey;
6
-
7
- if (data && angular.isObject(data)) {
8
- angular.forEach(data, function (value, key) {
9
- newKey = transform(key);
10
-
11
- if (newKey !== key) {
12
- data[newKey] = value;
13
- delete data[key];
14
- }
15
-
16
- transformObject(value, transform);
17
- });
18
- }
19
- }
20
-
21
- function camelize(key) {
22
- if (!angular.isString(key)) {
23
- return key;
24
- }
25
-
26
- // should this match more than word and digit characters?
27
- return key.replace(/_[\w\d]/g, function (match, index, string) {
28
- return index === 0 ? match : string.charAt(index + 1).toUpperCase();
29
- });
30
- }
31
-
32
- function underscore(key) {
33
- if (!angular.isString(key)) {
34
- return key;
35
- }
36
-
37
- return key.replace(/[A-Z]/g, function (match, index) {
38
- return index === 0 ? match : '_' + match.toLowerCase();
39
- });
40
- }
41
-
42
- angular.module('rails').factory('railsFieldRenamingTransformer', function () {
43
-
44
- return function (data) {
45
- transformObject(data, underscore);
46
- return data;
47
- };
48
- });
49
-
50
- angular.module('rails').factory('railsFieldRenamingInterceptor', function () {
51
- return function (promise) {
52
- return promise.then(function (response) {
53
- transformObject(response.data, camelize);
54
- return response;
55
- });
56
- };
57
- });
58
-
59
2
  angular.module('rails').factory('railsRootWrappingTransformer', function () {
60
3
  return function (data, resource) {
61
4
  var result = {};
@@ -84,110 +27,128 @@
84
27
  };
85
28
  });
86
29
 
87
- angular.module('rails').factory('railsResourceFactory', ['$http', '$q', '$injector', '$interpolate', function ($http, $q, $injector, $interpolate) {
88
- // urlBuilder("/path/{{someId}}")(someId: 5) == "/path/5"
89
- function urlBuilder(url) {
90
- var expression;
91
-
92
- if (angular.isFunction(url)) {
93
- return url;
94
- }
95
-
96
- if (url.indexOf('{{') === -1) {
97
- url = url + '/{{id}}';
98
- }
30
+ angular.module('rails').factory('railsResourceFactory', ['$http', '$q', 'railsUrlBuilder', 'railsSerializer', 'railsRootWrappingTransformer', 'railsRootWrappingInterceptor', 'RailsResourceInjector',
31
+ function ($http, $q, railsUrlBuilder, railsSerializer, railsRootWrappingTransformer, railsRootWrappingInterceptor, RailsResourceInjector) {
99
32
 
100
- expression = $interpolate(url);
33
+ function railsResourceFactory(config) {
34
+ var transformers = config.requestTransformers,
35
+ interceptors = config.responseInterceptors;
101
36
 
102
- return function (params) {
103
- url = expression(params);
37
+ function appendPath(url, path) {
38
+ if (path) {
39
+ if (path[0] !== '/') {
40
+ url += '/';
41
+ }
104
42
 
105
- if (url.charAt(url.length - 1) === '/') {
106
- url = url.substr(0, url.length - 1);
43
+ url += path;
107
44
  }
108
45
 
109
46
  return url;
110
- };
111
- }
112
-
113
- function railsResourceFactory(config) {
114
- var transformers = config.requestTransformers || ['railsRootWrappingTransformer', 'railsFieldRenamingTransformer'],
115
- interceptors = config.responseInterceptors || ['railsFieldRenamingInterceptor', 'railsRootWrappingInterceptor'];
47
+ }
116
48
 
117
49
  function RailsResource(value) {
118
- value || (value = {});
119
- var immediatePromise = function(data) {
120
- return {
121
- resource: RailsResource,
122
- response: data,
123
- then: function(callback) {
124
- this.response = callback(this.response, this.resource);
125
- return immediatePromise(this.response);
126
- }
127
- }
128
- };
129
- var data = RailsResource.callInterceptors(immediatePromise({data: value || {}})).response.data;
130
- angular.extend(this, data);
50
+ if (value) {
51
+ var immediatePromise = function(data) {
52
+ return {
53
+ resource: RailsResource,
54
+ response: data,
55
+ then: function(callback) {
56
+ this.response = callback(this.response, this.resource);
57
+ return immediatePromise(this.response);
58
+ }
59
+ }
60
+ };
61
+
62
+ var data = RailsResource.callInterceptors(immediatePromise({data: value})).response.data;
63
+ angular.extend(this, data);
64
+ }
131
65
  }
132
66
 
133
67
  RailsResource.setUrl = function(url) {
134
- RailsResource.url = urlBuilder(url);
68
+ RailsResource.url = railsUrlBuilder(url);
135
69
  };
136
70
  RailsResource.setUrl(config.url);
137
- RailsResource.rootName = config.name;
138
- RailsResource.rootPluralName = config.pluralName || config.name + 's';
71
+
72
+ RailsResource.enableRootWrapping = config.wrapData === undefined ? true : config.wrapData;
139
73
  RailsResource.httpConfig = config.httpConfig || {};
140
74
  RailsResource.httpConfig.headers = angular.extend({'Accept': 'application/json', 'Content-Type': 'application/json'}, RailsResource.httpConfig.headers || {});
141
75
  RailsResource.requestTransformers = [];
142
76
  RailsResource.responseInterceptors = [];
143
77
  RailsResource.defaultParams = config.defaultParams;
78
+ RailsResource.serializer = RailsResourceInjector.createService(config.serializer || railsSerializer());
79
+ RailsResource.rootName = RailsResource.serializer.underscore(config.name);
80
+ RailsResource.rootPluralName = RailsResource.serializer.underscore(config.pluralName || RailsResource.serializer.pluralize(config.name));
144
81
 
145
- // Add a function to run on response / initialize data
146
- // model methods and this are not yet available at this point
82
+ /**
83
+ * Add a callback to run on response and construction.
84
+ * @param fn(resource, constructor) - resource is a resource instance, and constructor is the resource class calling the function
85
+ */
147
86
  RailsResource.beforeResponse = function(fn) {
87
+ var fn = RailsResourceInjector.getDependency(fn);
148
88
  RailsResource.responseInterceptors.push(function(promise) {
149
89
  return promise.then(function(response) {
150
- fn(response.data, promise.resource);
151
- return response;
90
+ fn(response.data, promise.resource);
91
+ return response;
152
92
  });
153
93
  });
154
94
  };
155
95
 
156
- // Add a function to run on request data
96
+ /**
97
+ * Adds a function to run after serializing the data to send to the server, but before root-wrapping it.
98
+ * @param fn (data, constructor) - data object is the serialized resource instance, and constructor the resource class calling the function
99
+ */
157
100
  RailsResource.beforeRequest = function(fn) {
101
+ var fn = RailsResourceInjector.getDependency(fn);
158
102
  RailsResource.requestTransformers.push(function(data, resource) {
159
- fn(data, resource);
160
- return data;
103
+ return fn(data, resource) || data;
161
104
  });
162
105
  };
163
106
 
164
107
  // copied from $HttpProvider to support interceptors being dependency names or anonymous factory functions
165
108
  angular.forEach(interceptors, function (interceptor) {
166
- RailsResource.responseInterceptors.push(
167
- angular.isString(interceptor)
168
- ? $injector.get(interceptor)
169
- : $injector.invoke(interceptor)
170
- );
109
+ RailsResource.responseInterceptors.push(RailsResourceInjector.getDependency(interceptor));
171
110
  });
172
111
 
173
112
  angular.forEach(transformers, function (transformer) {
174
- RailsResource.requestTransformers.push(
175
- angular.isString(transformer)
176
- ? $injector.get(transformer)
177
- : $injector.invoke(transformer)
178
- );
113
+ RailsResource.requestTransformers.push(RailsResourceInjector.getDependency(transformer));
179
114
  });
180
115
 
116
+ // transform data for request:
181
117
  RailsResource.transformData = function (data) {
118
+ data = RailsResource.serializer.serialize(data);
119
+
120
+ // data is now serialized. call request transformers including beforeRequest
182
121
  angular.forEach(RailsResource.requestTransformers, function (transformer) {
183
122
  data = transformer(data, RailsResource);
184
123
  });
185
124
 
125
+
126
+ if (RailsResource.enableRootWrapping) {
127
+ data = railsRootWrappingTransformer(data, RailsResource);
128
+ }
129
+
186
130
  return data;
187
131
  };
188
132
 
133
+ // transform data on response:
189
134
  RailsResource.callInterceptors = function (promise) {
135
+ promise = promise.then(function (response) {
136
+ // store off the data in case something (like our root unwrapping) assigns data as a new object
137
+ response.originalData = response.data;
138
+ return response;
139
+ });
140
+
141
+ if (RailsResource.enableRootWrapping) {
142
+ promise.resource = RailsResource;
143
+ promise = railsRootWrappingInterceptor(promise);
144
+ }
190
145
 
146
+ promise.then(function (response) {
147
+ response.data = RailsResource.serializer.deserialize(response.data, RailsResource);
148
+ return response;
149
+ });
150
+
151
+ // data is now deserialized. call response interceptors including beforeResponse
191
152
  angular.forEach(RailsResource.responseInterceptors, function (interceptor) {
192
153
  promise.resource = RailsResource;
193
154
  promise = interceptor(promise);
@@ -197,25 +158,8 @@
197
158
  };
198
159
 
199
160
  RailsResource.processResponse = function (promise) {
200
- promise = RailsResource.callInterceptors(promise);
201
-
202
- return promise.then(function (response) {
203
- var result;
204
-
205
- if (angular.isArray(response.data)) {
206
- result = [];
207
-
208
- angular.forEach(response.data, function (value) {
209
- result.push(angular.extend(new RailsResource(), value));
210
- });
211
- } else if (angular.isObject(response.data)) {
212
- result = angular.extend(new RailsResource(), response.data);
213
-
214
- } else {
215
- result = response.data;
216
- }
217
-
218
- return result;
161
+ return RailsResource.callInterceptors(promise).then(function (response) {
162
+ return response.data;
219
163
  });
220
164
  };
221
165
 
@@ -255,14 +199,15 @@
255
199
  *
256
200
  * If the context is a number and the URL string does
257
201
  * @param context
202
+ * @param path {string} (optional) An additional path to append to the URL
258
203
  * @return {string}
259
204
  */
260
- RailsResource.$url = RailsResource.resourceUrl = function (context) {
205
+ RailsResource.$url = RailsResource.resourceUrl = function (context, path) {
261
206
  if (!angular.isObject(context)) {
262
207
  context = {id: context};
263
208
  }
264
209
 
265
- return RailsResource.url(context || {});
210
+ return appendPath(RailsResource.url(context || {}), path);
266
211
  };
267
212
 
268
213
  RailsResource.$get = function (url, queryParams) {
@@ -277,17 +222,17 @@
277
222
  return RailsResource.$get(RailsResource.resourceUrl(context), queryParams);
278
223
  };
279
224
 
280
- RailsResource.prototype.$url = function() {
281
- return RailsResource.resourceUrl(this);
225
+ /**
226
+ * Returns the URL for this resource.
227
+ *
228
+ * @param path {string} (optional) An additional path to append to the URL
229
+ * @returns {string} The URL for the resource
230
+ */
231
+ RailsResource.prototype.$url = function(path) {
232
+ return appendPath(RailsResource.resourceUrl(this), path);
282
233
  };
283
234
 
284
235
  RailsResource.prototype.processResponse = function (promise) {
285
- promise = promise.then(function (response) {
286
- // store off the data in case something (like our root unwrapping) assigns data as a new object
287
- response.originalData = response.data;
288
- return response;
289
- });
290
-
291
236
  promise = RailsResource.callInterceptors(promise);
292
237
 
293
238
  return promise.then(angular.bind(this, function (response) {
@@ -0,0 +1,471 @@
1
+ (function (undefined) {
2
+ angular.module('rails').factory('railsSerializer', ['$injector', 'RailsInflector', 'RailsResourceInjector', function ($injector, RailsInflector, RailsResourceInjector) {
3
+ var defaultOptions = {
4
+ underscore: RailsInflector.underscore,
5
+ camelize: RailsInflector.camelize,
6
+ pluralize: RailsInflector.pluralize,
7
+ exclusionMatchers: ['$'],
8
+ excludeByDefault: false
9
+ };
10
+
11
+ function railsSerializer(options, customizer) {
12
+
13
+ function Serializer() {
14
+ if (angular.isFunction(options)) {
15
+ customizer = options;
16
+ options = {};
17
+ }
18
+
19
+ this.exclusions = {};
20
+ this.inclusions = {};
21
+ this.serializeMappings = {};
22
+ this.deserializeMappings = {};
23
+ this.customSerializedAttributes = {};
24
+ this.preservedAttributes = {};
25
+ this.customSerializers = {};
26
+ this.nestedResources = {};
27
+ this.options = angular.extend({}, defaultOptions, options || {});
28
+
29
+ if (customizer) {
30
+ customizer.call(this, this);
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Accepts a variable list of attribute names to exclude from JSON serialization.
36
+ *
37
+ * @param attributeNames... {string} Variable number of attribute name parameters
38
+ * @returns {Serializer} this for chaining support
39
+ */
40
+ Serializer.prototype.exclude = function () {
41
+ var exclusions = this.exclusions;
42
+
43
+ angular.forEach(arguments, function (attributeName) {
44
+ exclusions[attributeName] = false;
45
+ });
46
+
47
+ return this;
48
+ };
49
+
50
+ /**
51
+ * Accepts a variable list of attribute names that should be included in JSON serialization.
52
+ * Using this method will by default exclude all other attributes and only the ones explicitly included using <code>only</code> will be serialized.
53
+ * @param attributeNames... {string} Variable number of attribute name parameters
54
+ * @returns {Serializer} this for chaining support
55
+ */
56
+ Serializer.prototype.only = function () {
57
+ var inclusions = this.inclusions;
58
+ this.options.excludeByDefault = true;
59
+
60
+ angular.forEach(arguments, function (attributeName) {
61
+ inclusions[attributeName] = true;
62
+ });
63
+
64
+ return this;
65
+ };
66
+
67
+ /**
68
+ * This is a shortcut for rename that allows you to specify a variable number of attributes that should all be renamed to
69
+ * <code>{attributeName}_attributes</code> to work with the Rails nested_attributes feature.
70
+ * @param attributeNames... {string} Variable number of attribute name parameters
71
+ * @returns {Serializer} this for chaining support
72
+ */
73
+ Serializer.prototype.nestedAttribute = function () {
74
+ var self = this;
75
+
76
+ angular.forEach(arguments, function (attributeName) {
77
+ self.rename(attributeName, attributeName + '_attributes');
78
+ });
79
+
80
+ return this;
81
+ };
82
+
83
+ /**
84
+ * Specifies an attribute that is a nested resource within the parent object.
85
+ * Nested resources do not imply nested attributes, if you want both you still have to specify call <code>nestedAttribute</code> as well.
86
+ *
87
+ * A nested resource serves two purposes. First, it defines the resource that should be used when constructing resources from the server.
88
+ * Second, it specifies how the nested object should be serialized.
89
+ *
90
+ * An optional third parameter <code>serializer</code> is available to override the serialization logic
91
+ * of the resource in case you need to serialize it differently in multiple contexts.
92
+ *
93
+ * @param attributeName {string} The name of the attribute that is a nested resource
94
+ * @param resource {string | Resource} A reference to the resource that the attribute is a type of.
95
+ * @param serializer {string | Serializer} (optional) An optional serializer reference to override the nested resource's default serializer
96
+ * @returns {Serializer} this for chaining support
97
+ */
98
+ Serializer.prototype.resource = function (attributeName, resource, serializer) {
99
+ this.nestedResources[attributeName] = resource;
100
+
101
+ if (serializer) {
102
+ this.serializeWith(attributeName, serializer);
103
+ }
104
+
105
+ return this;
106
+ };
107
+
108
+ /**
109
+ * Specifies a custom name mapping for an attribute.
110
+ * On serializing to JSON the jsonName will be used.
111
+ * On deserialization, if jsonName is seen then it will be renamed as javascriptName in the resulting resource.
112
+ *
113
+ * @param javascriptName {string} The attribute name as it appears in the JavaScript object
114
+ * @param jsonName {string} The attribute name as it should appear in JSON
115
+ * @param bidirectional {boolean} (optional) Allows turning off the bidirectional renaming, defaults to true.
116
+ * @returns {Serializer} this for chaining support
117
+ */
118
+ Serializer.prototype.rename = function (javascriptName, jsonName, bidirectional) {
119
+ this.serializeMappings[javascriptName] = jsonName;
120
+
121
+ if (bidirectional || bidirectional === undefined) {
122
+ this.deserializeMappings[jsonName] = javascriptName;
123
+ }
124
+ return this;
125
+ };
126
+
127
+ /**
128
+ * Allows custom attribute creation as part of the serialization to JSON.
129
+ *
130
+ * @param attributeName {string} The name of the attribute to add
131
+ * @param value {*} The value to add, if specified as a function then the function will be called during serialization
132
+ * and should return the value to add.
133
+ * @returns {Serializer} this for chaining support
134
+ */
135
+ Serializer.prototype.add = function (attributeName, value) {
136
+ this.customSerializedAttributes[attributeName] = value;
137
+ return this;
138
+ };
139
+
140
+
141
+ /**
142
+ * Allows the attribute to be preserved unmodified in the resulting object.
143
+ *
144
+ * @param attributeName {string} The name of the attribute to add
145
+ * @returns {Serializer} this for chaining support
146
+ */
147
+ Serializer.prototype.preserve = function(attributeName) {
148
+ this.preservedAttributes[attributeName] = true;
149
+ return this;
150
+ };
151
+
152
+ /**
153
+ * Specify a custom serializer to use for an attribute.
154
+ *
155
+ * @param attributeName {string} The name of the attribute
156
+ * @param serializer {string | constructor} A reference to the custom serializer to use for the attribute.
157
+ * @returns {Serializer} this for chaining support
158
+ */
159
+ Serializer.prototype.serializeWith = function (attributeName, serializer) {
160
+ this.customSerializers[attributeName] = serializer;
161
+ return this;
162
+ };
163
+
164
+ /**
165
+ * Determines whether or not an attribute should be excluded.
166
+ *
167
+ * If the option excludeByDefault has been set then attributes will default to excluded and will only
168
+ * be included if they have been included using the "only" customization function.
169
+ *
170
+ * If the option excludeByDefault has not been set then attributes must be explicitly excluded using the "exclude"
171
+ * customization function or must be matched by one of the exclusionMatchers.
172
+ *
173
+ * @param attributeName The name of the attribute to check for exclusion
174
+ * @returns {boolean} true if excluded, false otherwise
175
+ */
176
+ Serializer.prototype.isExcludedFromSerialization = function (attributeName) {
177
+ if ((this.options.excludeByDefault && !this.inclusions.hasOwnProperty(attributeName)) || this.exclusions.hasOwnProperty(attributeName)) {
178
+ return true;
179
+ }
180
+
181
+ if (this.options.exclusionMatchers) {
182
+ var excluded = false;
183
+
184
+ angular.forEach(this.options.exclusionMatchers, function (matcher) {
185
+ if (angular.isString(matcher)) {
186
+ excluded = excluded || attributeName.indexOf(matcher) === 0;
187
+ } else if (angular.isFunction(matcher)) {
188
+ excluded = excluded || matcher.call(undefined, attributeName);
189
+ } else if (matcher instanceof RegExp) {
190
+ excluded = excluded || matcher.test(attributeName);
191
+ }
192
+ });
193
+
194
+ return excluded;
195
+ }
196
+
197
+ return false;
198
+ };
199
+
200
+ /**
201
+ * Remaps the attribute name to the serialized form which includes:
202
+ * - checking for exclusion
203
+ * - remapping to a custom value specified by the rename customization function
204
+ * - underscoring the name
205
+ *
206
+ * @param attributeName The current attribute name
207
+ * @returns {*} undefined if the attribute should be excluded or the mapped attribute name
208
+ */
209
+ Serializer.prototype.getSerializedAttributeName = function (attributeName) {
210
+ var mappedName = this.serializeMappings[attributeName] || attributeName;
211
+
212
+ if (this.isExcludedFromSerialization(attributeName) || this.isExcludedFromSerialization(mappedName)) {
213
+ return undefined;
214
+ }
215
+
216
+ return this.underscore(mappedName);
217
+ };
218
+
219
+ /**
220
+ * Determines whether or not an attribute should be excluded from deserialization.
221
+ *
222
+ * By default, we do not exclude any attributes from deserialization.
223
+ *
224
+ * @param attributeName The name of the attribute to check for exclusion
225
+ * @returns {boolean} true if excluded, false otherwise
226
+ */
227
+ Serializer.prototype.isExcludedFromDeserialization = function (attributeName) {
228
+ return false;
229
+ };
230
+
231
+ /**
232
+ * Remaps the attribute name to the deserialized form which includes:
233
+ * - camelizing the name
234
+ * - checking for exclusion
235
+ * - remapping to a custom value specified by the rename customization function
236
+ *
237
+ * @param attributeName The current attribute name
238
+ * @returns {*} undefined if the attribute should be excluded or the mapped attribute name
239
+ */
240
+ Serializer.prototype.getDeserializedAttributeName = function (attributeName) {
241
+ var camelizedName = this.camelize(attributeName);
242
+
243
+ camelizedName = this.deserializeMappings[attributeName]
244
+ || this.deserializeMappings[camelizedName]
245
+ || camelizedName;
246
+
247
+ if (this.isExcludedFromDeserialization(attributeName) || this.isExcludedFromDeserialization(camelizedName)) {
248
+ return undefined;
249
+ }
250
+
251
+ return camelizedName;
252
+ };
253
+
254
+ /**
255
+ * Returns a reference to the nested resource that has been specified for the attribute.
256
+ * @param attributeName The attribute name
257
+ * @returns {*} undefined if no nested resource has been specified or a reference to the nested resource class
258
+ */
259
+ Serializer.prototype.getNestedResource = function (attributeName) {
260
+ return RailsResourceInjector.getDependency(this.nestedResources[attributeName]);
261
+ };
262
+
263
+ /**
264
+ * Returns a custom serializer for the attribute if one has been specified. Custom serializers can be specified
265
+ * in one of two ways. The serializeWith customization method allows specifying a custom serializer for any attribute.
266
+ * Or an attribute could have been specified as a nested resource in which case the nested resource's serializer
267
+ * is used. Custom serializers specified using serializeWith take precedence over the nested resource serializer.
268
+ *
269
+ * @param attributeName The attribute name
270
+ * @returns {*} undefined if no custom serializer has been specified or an instance of the Serializer
271
+ */
272
+ Serializer.prototype.getAttributeSerializer = function (attributeName) {
273
+ var resource = this.getNestedResource(attributeName),
274
+ serializer = this.customSerializers[attributeName];
275
+
276
+ // custom serializer takes precedence over resource serializer
277
+ if (serializer) {
278
+ return RailsResourceInjector.createService(serializer)
279
+ } else if (resource) {
280
+ return resource.serializer;
281
+ }
282
+
283
+ return undefined;
284
+ };
285
+
286
+
287
+ /**
288
+ * Prepares the data for serialization to JSON.
289
+ *
290
+ * @param data The data to prepare
291
+ * @returns {*} A new object or array that is ready for JSON serialization
292
+ */
293
+ Serializer.prototype.serializeValue = function (data) {
294
+ var result = data,
295
+ self = this;
296
+
297
+ if (angular.isArray(data)) {
298
+ result = [];
299
+
300
+ angular.forEach(data, function (value) {
301
+ result.push(self.serializeValue(value));
302
+ });
303
+ } else if (angular.isObject(data)) {
304
+ if (angular.isDate(data)) {
305
+ return data;
306
+ }
307
+ result = {};
308
+
309
+ angular.forEach(data, function (value, key) {
310
+ // if the value is a function then it can't be serialized to JSON so we'll just skip it
311
+ if (!angular.isFunction(value)) {
312
+ self.serializeAttribute(result, key, value);
313
+ }
314
+ });
315
+ }
316
+
317
+ return result;
318
+ };
319
+
320
+ /**
321
+ * Transforms an attribute and its value and stores it on the parent data object. The attribute will be
322
+ * renamed as needed and the value itself will be serialized as well.
323
+ *
324
+ * @param data The object that the attribute will be added to
325
+ * @param attribute The attribute to transform
326
+ * @param value The current value of the attribute
327
+ */
328
+ Serializer.prototype.serializeAttribute = function (data, attribute, value) {
329
+ var serializer = this.getAttributeSerializer(attribute),
330
+ serializedAttributeName = this.getSerializedAttributeName(attribute);
331
+
332
+ // undefined means the attribute should be excluded from serialization
333
+ if (serializedAttributeName === undefined) {
334
+ return;
335
+ }
336
+
337
+ data[serializedAttributeName] = serializer ? serializer.serialize(value) : this.serializeValue(value);
338
+ };
339
+
340
+ /**
341
+ * Serializes the data by applying various transformations such as:
342
+ * - Underscoring attribute names
343
+ * - attribute renaming
344
+ * - attribute exclusion
345
+ * - custom attribute addition
346
+ *
347
+ * @param data The data to prepare
348
+ * @returns {*} A new object or array that is ready for JSON serialization
349
+ */
350
+ Serializer.prototype.serialize = function (data) {
351
+ var result = this.serializeValue(data),
352
+ self = this;
353
+
354
+ if (angular.isObject(result)) {
355
+ angular.forEach(this.customSerializedAttributes, function (value, key) {
356
+ if (angular.isFunction(value)) {
357
+ value = value.call(data, data);
358
+ }
359
+
360
+ self.serializeAttribute(result, key, value);
361
+ });
362
+ }
363
+
364
+ return result;
365
+ };
366
+
367
+ /**
368
+ * Iterates over the data deserializing each entry on arrays and each key/value on objects.
369
+ *
370
+ * @param data The object to deserialize
371
+ * @param Resource (optional) The resource type to deserialize the result into
372
+ * @returns {*} A new object or an instance of Resource populated with deserialized data.
373
+ */
374
+ Serializer.prototype.deserializeValue = function (data, Resource) {
375
+ var result = data,
376
+ self = this;
377
+
378
+ if (angular.isArray(data)) {
379
+ result = [];
380
+
381
+ angular.forEach(data, function (value) {
382
+ result.push(self.deserializeValue(value, Resource));
383
+ });
384
+ } else if (angular.isObject(data)) {
385
+ result = {};
386
+
387
+ if (Resource) {
388
+ result = new Resource();
389
+ }
390
+
391
+ angular.forEach(data, function (value, key) {
392
+ self.deserializeAttribute(result, key, value);
393
+ });
394
+ }
395
+
396
+ return result;
397
+ };
398
+
399
+ /**
400
+ * Transforms an attribute and its value and stores it on the parent data object. The attribute will be
401
+ * renamed as needed and the value itself will be deserialized as well.
402
+ *
403
+ * @param data The object that the attribute will be added to
404
+ * @param attribute The attribute to transform
405
+ * @param value The current value of the attribute
406
+ */
407
+ Serializer.prototype.deserializeAttribute = function (data, attribute, value) {
408
+ var serializer,
409
+ NestedResource,
410
+ attributeName = this.getDeserializedAttributeName(attribute);
411
+
412
+ // undefined means the attribute should be excluded from serialization
413
+ if (attributeName === undefined) {
414
+ return;
415
+ }
416
+
417
+ serializer = this.getAttributeSerializer(attributeName);
418
+ NestedResource = this.getNestedResource(attributeName);
419
+
420
+ // preserved attributes are assigned unmodified
421
+ if (this.preservedAttributes[attributeName]) {
422
+ data[attributeName] = value;
423
+ } else {
424
+ data[attributeName] = serializer ? serializer.deserialize(value, NestedResource) : this.deserializeValue(value, NestedResource);
425
+ }
426
+ };
427
+
428
+ /**
429
+ * Deserializes the data by applying various transformations such as:
430
+ * - Camelizing attribute names
431
+ * - attribute renaming
432
+ * - attribute exclusion
433
+ * - nested resource creation
434
+ *
435
+ * @param data The object to deserialize
436
+ * @param Resource (optional) The resource type to deserialize the result into
437
+ * @returns {*} A new object or an instance of Resource populated with deserialized data
438
+ */
439
+ Serializer.prototype.deserialize = function (data, Resource) {
440
+ // just calls deserializeValue for now so we can more easily add on custom attribute logic for deserialize too
441
+ return this.deserializeValue(data, Resource);
442
+ };
443
+
444
+ Serializer.prototype.pluralize = function (value) {
445
+ if (this.options.pluralize) {
446
+ return this.options.pluralize(value);
447
+ }
448
+ return value;
449
+ };
450
+
451
+ Serializer.prototype.underscore = function (value) {
452
+ if (this.options.underscore) {
453
+ return this.options.underscore(value);
454
+ }
455
+ return value;
456
+ };
457
+
458
+ Serializer.prototype.camelize = function (value) {
459
+ if (this.options.camelize) {
460
+ return this.options.camelize(value);
461
+ }
462
+ return value;
463
+ }
464
+
465
+ return Serializer;
466
+ }
467
+
468
+ railsSerializer.defaultOptions = defaultOptions;
469
+ return railsSerializer;
470
+ }]);
471
+ }());