api_resource 0.6.18 → 0.6.19
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 +4 -4
- data/Gemfile.lock +4 -8
- data/Guardfile +5 -17
- data/api_resource.gemspec +4 -7
- data/lib/api_resource.rb +20 -19
- data/lib/api_resource/associations.rb +39 -23
- data/lib/api_resource/associations/association_proxy.rb +14 -13
- data/lib/api_resource/attributes.rb +555 -156
- data/lib/api_resource/base.rb +376 -305
- data/lib/api_resource/connection.rb +22 -12
- data/lib/api_resource/finders.rb +17 -18
- data/lib/api_resource/finders/single_finder.rb +1 -1
- data/lib/api_resource/mocks.rb +37 -31
- data/lib/api_resource/scopes.rb +70 -12
- data/lib/api_resource/serializer.rb +264 -0
- data/lib/api_resource/typecast.rb +13 -2
- data/lib/api_resource/typecasters/unknown_typecaster.rb +33 -0
- data/lib/api_resource/version.rb +1 -1
- data/spec/lib/associations/has_many_remote_object_proxy_spec.rb +3 -3
- data/spec/lib/associations_spec.rb +49 -94
- data/spec/lib/attributes_spec.rb +40 -56
- data/spec/lib/base_spec.rb +290 -382
- data/spec/lib/callbacks_spec.rb +6 -6
- data/spec/lib/connection_spec.rb +20 -20
- data/spec/lib/finders_spec.rb +14 -0
- data/spec/lib/mocks_spec.rb +9 -9
- data/spec/lib/prefixes_spec.rb +4 -5
- data/spec/lib/scopes_spec.rb +98 -0
- data/spec/lib/serializer_spec.rb +156 -0
- data/spec/spec_helper.rb +1 -4
- data/spec/support/test_resource.rb +1 -1
- metadata +14 -38
- data/spec/tmp/DIR +0 -0
@@ -1,35 +1,54 @@
|
|
1
1
|
module ApiResource
|
2
2
|
|
3
|
+
#
|
4
|
+
# Error raised when accessing an attribute in an invalid
|
5
|
+
# way such as trying to write to a protected attribute
|
6
|
+
#
|
7
|
+
# @author [ejlangev]
|
8
|
+
#
|
9
|
+
class AttributeAccessError < NoMethodError
|
10
|
+
end
|
11
|
+
|
3
12
|
module Attributes
|
4
|
-
|
13
|
+
|
5
14
|
extend ActiveSupport::Concern
|
15
|
+
|
6
16
|
include ActiveModel::AttributeMethods
|
7
17
|
include ActiveModel::Dirty
|
8
|
-
|
18
|
+
|
9
19
|
included do
|
10
20
|
|
11
21
|
# include ApiResource::Typecast if it isn't already
|
12
22
|
include ApiResource::Typecast
|
13
|
-
|
23
|
+
|
14
24
|
alias_method_chain :save, :dirty_tracking
|
15
|
-
|
25
|
+
|
26
|
+
# Set up some class attributes for managing all of
|
27
|
+
# this
|
16
28
|
class_attribute(
|
17
|
-
:attribute_names,
|
18
|
-
:public_attribute_names,
|
19
|
-
:protected_attribute_names,
|
20
|
-
:attribute_types
|
29
|
+
:attribute_names,
|
30
|
+
:public_attribute_names,
|
31
|
+
:protected_attribute_names,
|
32
|
+
:attribute_types,
|
33
|
+
:primary_key,
|
34
|
+
:attribute_method_module
|
21
35
|
)
|
22
|
-
|
36
|
+
# Initialize those class attributes
|
23
37
|
self.attribute_names = []
|
24
38
|
self.public_attribute_names = []
|
25
39
|
self.protected_attribute_names = []
|
26
40
|
self.attribute_types = {}.with_indifferent_access
|
27
|
-
|
28
41
|
|
42
|
+
self.primary_key = :id
|
43
|
+
self.attribute_method_module = Module.new
|
44
|
+
|
45
|
+
include self.attribute_method_module
|
29
46
|
# This method is important for reloading an object. If the
|
30
47
|
# object has already been loaded, its associations will trip
|
31
48
|
# up the load method unless we pass in the internal objects.
|
32
|
-
|
49
|
+
#
|
50
|
+
# TODO: This seems like kind of a hack that shouldn't be
|
51
|
+
# necessary. Remove it at some point during the refactoring
|
33
52
|
define_method(:attributes_without_proxies) do
|
34
53
|
attributes = @attributes
|
35
54
|
|
@@ -56,160 +75,460 @@ module ApiResource
|
|
56
75
|
|
57
76
|
attributes
|
58
77
|
end
|
59
|
-
|
78
|
+
|
60
79
|
end
|
61
|
-
|
80
|
+
|
62
81
|
module ClassMethods
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
82
|
+
#
|
83
|
+
# Wrapper method to define all of the attributes (public and protected)
|
84
|
+
# for this Class. This should be called with a locked mutex to
|
85
|
+
# prevent multiple threads from defining attributes at the same time
|
86
|
+
#
|
87
|
+
# @return [Boolean] true
|
88
|
+
def define_all_attributes
|
89
|
+
if self.resource_definition["attributes"]
|
90
|
+
# First we need to clear out the old values and clone them
|
91
|
+
self.attribute_names = []
|
92
|
+
self.public_attribute_names = []
|
93
|
+
self.protected_attribute_names = []
|
94
|
+
self.attribute_types = {}.with_indifferent_access
|
95
|
+
# First define all public attributes
|
96
|
+
define_attributes(
|
97
|
+
*self.resource_definition['attributes']['public'],
|
98
|
+
access_level: :public
|
99
|
+
)
|
100
|
+
# Then define all private attributes
|
101
|
+
define_attributes(
|
102
|
+
*self.resource_definition['attributes']['protected'],
|
103
|
+
access_level: :protected
|
104
|
+
)
|
67
105
|
end
|
68
|
-
|
69
|
-
self.public_attribute_names.uniq!
|
106
|
+
true
|
70
107
|
end
|
71
|
-
|
72
|
-
|
108
|
+
|
109
|
+
#
|
110
|
+
# Sets up the attributes for this class, can be called multiple
|
111
|
+
# times to add more attributes. Again meant to be called with a locked
|
112
|
+
# mutex for thread safety
|
113
|
+
#
|
114
|
+
# @param *args [Array] List of attributes to define and optional
|
115
|
+
# hash as a last parameter
|
116
|
+
#
|
117
|
+
# @return [Boolean] Always true
|
118
|
+
def define_attributes(*args)
|
119
|
+
options = args.extract_options!
|
120
|
+
options[:access_level] ||= :public
|
121
|
+
# Initialize each attribute
|
73
122
|
args.each do |arg|
|
74
|
-
self.
|
123
|
+
self.initialize_attribute(
|
124
|
+
Array.wrap(arg),
|
125
|
+
options[:access_level]
|
126
|
+
)
|
75
127
|
end
|
76
|
-
|
77
|
-
self.
|
128
|
+
|
129
|
+
self.define_attribute_methods(
|
130
|
+
args,
|
131
|
+
options[:access_level]
|
132
|
+
)
|
78
133
|
end
|
79
134
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
135
|
+
#
|
136
|
+
# Adds the attribute into some internal data structures but does
|
137
|
+
# not define any methods for it
|
138
|
+
#
|
139
|
+
# @param arg [Array] A 1 or 2 element array holding an attribute name and
|
140
|
+
# optionally a type for that attribute
|
141
|
+
# @param access_level [Symbol] Either :protected or :public based on
|
142
|
+
# the access level for this attribute
|
143
|
+
#
|
144
|
+
# @return [Boolean] Always true
|
145
|
+
def initialize_attribute(attr, access_level)
|
146
|
+
attr_name = attr[0].to_sym
|
147
|
+
attr_type = (attr[1] || :unknown).to_sym
|
148
|
+
|
149
|
+
# Look for the typecaster, raise an error if one is not found
|
150
|
+
typecaster = self.typecasters[attr_type]
|
151
|
+
if typecaster.nil?
|
152
|
+
raise TypecasterNotFound, "#{attr_type} is an unknown type"
|
153
|
+
end
|
154
|
+
# Map the attribute name to the typecaster
|
155
|
+
self.attribute_types[attr_name] = typecaster
|
156
|
+
# Add the attribute to the proper list
|
157
|
+
if access_level == :public
|
158
|
+
if self.protected_attribute?(attr_name)
|
159
|
+
raise ArgumentError, "Illegal change of attribute access level for #{attr_name}"
|
89
160
|
end
|
90
|
-
|
91
|
-
|
92
|
-
|
161
|
+
|
162
|
+
self.public_attribute_names << attr_name
|
163
|
+
else
|
164
|
+
if self.public_attribute?(attr_name)
|
165
|
+
raise ArgumentError, "Illegal change of attribute access level for #{attr_name}"
|
93
166
|
end
|
94
|
-
|
95
|
-
|
96
|
-
|
167
|
+
|
168
|
+
self.protected_attribute_names << attr_name
|
169
|
+
end
|
170
|
+
self.attribute_names << attr_name
|
171
|
+
true
|
97
172
|
end
|
98
173
|
|
99
|
-
|
100
|
-
|
101
|
-
|
174
|
+
#
|
175
|
+
# Defines the attribute methods in a new module which
|
176
|
+
# is then included in this class. Meant to be called with
|
177
|
+
# a locked mutex
|
178
|
+
#
|
179
|
+
# @param args [Array] List of attributes to define
|
180
|
+
# @param access_level [Symbol] :protected or :public
|
181
|
+
#
|
182
|
+
# @return [Boolean] Always true
|
183
|
+
def define_attribute_methods(attrs, access_level)
|
184
|
+
self.attribute_method_module.module_eval do
|
185
|
+
attrs.each do |attr|
|
186
|
+
# Normalize for attributes without types
|
187
|
+
attr_name = Array.wrap(attr).first
|
188
|
+
# Define reader and huh methods
|
189
|
+
self.module_eval <<-EOE, __FILE__, __LINE__ + 1
|
190
|
+
def #{attr_name}
|
191
|
+
read_attribute(:#{attr_name})
|
192
|
+
end
|
193
|
+
|
194
|
+
def #{attr_name}?
|
195
|
+
read_attribute(:#{attr_name}).present?
|
196
|
+
end
|
197
|
+
EOE
|
198
|
+
# Define a writer if this is public
|
199
|
+
if access_level == :public
|
200
|
+
self.module_eval <<-EOE, __FILE__, __LINE__ + 1
|
201
|
+
def #{attr_name}=(new_val)
|
202
|
+
write_attribute(:#{attr_name}, new_val)
|
203
|
+
end
|
204
|
+
EOE
|
205
|
+
end
|
206
|
+
end
|
102
207
|
end
|
103
|
-
|
208
|
+
|
209
|
+
# Now we get all the attribute names as symbols
|
210
|
+
# and define_attribute_methods for dirty tracking
|
211
|
+
attrs = attrs.collect do |a|
|
212
|
+
Array.wrap(a).first.to_sym
|
213
|
+
end
|
214
|
+
|
215
|
+
super(attrs)
|
216
|
+
true
|
104
217
|
end
|
105
|
-
|
106
|
-
|
218
|
+
|
219
|
+
#
|
220
|
+
# Returns true if the provided name is an attribute
|
221
|
+
# of this class
|
222
|
+
#
|
223
|
+
# @param name [Symbol] The name of the potential attribute
|
224
|
+
#
|
225
|
+
# @return [Boolean] True if an attribute with the given name exists
|
107
226
|
def attribute?(name)
|
108
227
|
self.attribute_names.include?(name.to_sym)
|
109
228
|
end
|
110
|
-
|
229
|
+
|
230
|
+
#
|
231
|
+
# Returns true if the provided name is a protected attribute
|
232
|
+
# @param name [Symbol] Name of the potential attribute
|
233
|
+
#
|
234
|
+
# @return [Boolean] True if a protected attribute with the given name exists
|
111
235
|
def protected_attribute?(name)
|
112
236
|
self.protected_attribute_names.include?(name.to_sym)
|
113
237
|
end
|
114
|
-
|
238
|
+
|
239
|
+
#
|
240
|
+
# Returns true if the provided name is a public attribute
|
241
|
+
# @param name [Symbol] Name of the potential attribute
|
242
|
+
#
|
243
|
+
# @return [Boolean] True if a public attribute with the given name exists
|
244
|
+
def public_attribute?(name)
|
245
|
+
self.public_attribute_names.include?(name.to_sym)
|
246
|
+
end
|
247
|
+
|
248
|
+
#
|
249
|
+
# Removes all attributes from this class but does _NOT_
|
250
|
+
# undefine their methods
|
251
|
+
#
|
252
|
+
# @return [Boolean] Always true
|
115
253
|
def clear_attributes
|
116
254
|
self.attribute_names.clear
|
117
255
|
self.public_attribute_names.clear
|
118
256
|
self.protected_attribute_names.clear
|
119
|
-
end
|
120
257
|
|
121
|
-
|
122
|
-
# attributes we are creating
|
123
|
-
def store_attribute_data(arg, type)
|
124
|
-
if arg.is_a?(Array)
|
125
|
-
self.define_attribute_type(arg.first, arg.second)
|
126
|
-
arg = arg.first
|
127
|
-
end
|
128
|
-
self.attribute_names += [arg.to_sym]
|
129
|
-
self.send(
|
130
|
-
"#{type}_attribute_names=",
|
131
|
-
self.send("#{type}_attribute_names") + [arg.to_sym]
|
132
|
-
)
|
133
|
-
self.define_accessor_methods(arg)
|
258
|
+
true
|
134
259
|
end
|
135
260
|
|
136
261
|
end
|
137
262
|
|
138
|
-
#
|
263
|
+
#
|
264
|
+
# Override for initialize to set up attribute and
|
265
|
+
# attributes cache
|
266
|
+
#
|
267
|
+
# @param *args [Array] Arguments to initialize,
|
268
|
+
# ignored in this case
|
269
|
+
#
|
270
|
+
# @return [Object] The object in question
|
139
271
|
def initialize(*args)
|
140
|
-
@attributes =
|
272
|
+
@attributes = HashWithIndifferentAccess[ self.class.attribute_names.zip([]) ]
|
273
|
+
@attributes_cache = HashWithIndifferentAccess.new
|
274
|
+
@previously_changed = HashWithIndifferentAccess.new
|
275
|
+
@changed_attributes = HashWithIndifferentAccess.new
|
276
|
+
super()
|
141
277
|
end
|
142
278
|
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
279
|
+
#
|
280
|
+
# Reads an attribute typecasting if necessary
|
281
|
+
# and setting the cache so as to only typecast
|
282
|
+
# the one time. Takes a block which is called if the
|
283
|
+
# attribute is not found
|
284
|
+
#
|
285
|
+
# @param attr_name [Symbol] The name of the attribute to read
|
286
|
+
#
|
287
|
+
# @return [Object] The value of the attribute or nil if it
|
288
|
+
# is not found
|
289
|
+
def read_attribute(attr_name)
|
290
|
+
attr_name = attr_name.to_sym
|
148
291
|
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
292
|
+
@attributes_cache[attr_name] || @attributes_cache.fetch(attr_name) do
|
293
|
+
data = @attributes.fetch(attr_name) do
|
294
|
+
# This statement overrides id to return the primary key
|
295
|
+
# if it is set to something other than :id
|
296
|
+
if attr_name == :id && self.class.primary_key != attr_name
|
297
|
+
return read_attribute(self.class.primary_key)
|
298
|
+
end
|
299
|
+
|
300
|
+
# For some reason hashes return false for key? if the value
|
301
|
+
# at that key is nil. It also executes this block for fetch
|
302
|
+
# when it really shouldn't. Detect that error here and give
|
303
|
+
# back nil for data
|
304
|
+
if @attributes.keys.include?(attr_name)
|
305
|
+
nil
|
306
|
+
else
|
307
|
+
# In this case the attribute was truly not found, if we're
|
308
|
+
# given a block execute that otherwise return nil
|
309
|
+
return block_given? ? yield(attr_name) : nil
|
310
|
+
end
|
156
311
|
end
|
157
|
-
|
312
|
+
# This sets and returns the typecasted value
|
313
|
+
@attributes_cache[attr_name] = self.typecast_attribute_for_read(
|
314
|
+
attr_name,
|
315
|
+
data
|
316
|
+
)
|
158
317
|
end
|
159
|
-
new_attrs
|
160
318
|
end
|
161
319
|
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
320
|
+
#
|
321
|
+
# Reads the attribute directly out of the attributes hash
|
322
|
+
# without applying any typecasting
|
323
|
+
# @param attr_name [Symbol] The name of the attribute to be read
|
324
|
+
#
|
325
|
+
# @return [Object] The untypecasted value of the attribute or nil
|
326
|
+
# if that attribute is not found
|
327
|
+
def read_attribute_before_type_cast(attr_name)
|
328
|
+
return @attributes[attr_name.to_sym]
|
329
|
+
end
|
330
|
+
|
331
|
+
#
|
332
|
+
# Writes an attribute, first typecasting it to the proper type with
|
333
|
+
# typecast_attribute_for_write and then setting it in the attributes
|
334
|
+
# hash. Raises MissingAttributeError if no such attribute exists
|
335
|
+
#
|
336
|
+
# @param attr_name [Symbol] The name of the attribute to set
|
337
|
+
# @param value [Object] The value to write
|
338
|
+
#
|
339
|
+
# @return [Object] The value parameter is always returned
|
340
|
+
def write_attribute(attr_name, value)
|
341
|
+
attr_name = attr_name.to_sym
|
342
|
+
# Change a write attribute for id to the primary key
|
343
|
+
attr_name = self.class.primary_key if attr_name == :id && self.class.primary_key
|
344
|
+
# The value we expect here should be typecasted for going to
|
345
|
+
# the api
|
346
|
+
typed_value = self.typecast_attribute_for_write(attr_name, value)
|
347
|
+
|
348
|
+
if attribute_changed?(attr_name)
|
349
|
+
old = changed_attributes[attr_name]
|
350
|
+
changed_attributes.delete(attr_name) if old == typed_value
|
167
351
|
else
|
168
|
-
|
352
|
+
old = clone_attribute_value(:read_attribute, attr_name)
|
353
|
+
changed_attributes[attr_name] = old if old != typed_value
|
169
354
|
end
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
355
|
+
|
356
|
+
# Remove this attribute from the attributes cache
|
357
|
+
@attributes_cache.delete(attr_name)
|
358
|
+
# Raise an error if this is not an attribute
|
359
|
+
if !self.attribute?(attr_name)
|
360
|
+
raise ActiveModel::MissingAttributeError.new(
|
361
|
+
"can't write unknown attribute #{attr_name}",
|
362
|
+
caller(0)
|
363
|
+
)
|
176
364
|
end
|
365
|
+
# Raise another error if this is a protected attribute
|
366
|
+
# if self.protected_attribute?(attr_name)
|
367
|
+
# raise ApiResource::AttributeAccessError.new(
|
368
|
+
# "cannot write to protected attribute #{attr_name}",
|
369
|
+
# caller(0)
|
370
|
+
# )
|
371
|
+
# end
|
372
|
+
@attributes[attr_name] = typed_value
|
373
|
+
value
|
177
374
|
end
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
375
|
+
|
376
|
+
#
|
377
|
+
# Returns the typecasted value of an attribute for being
|
378
|
+
# read (calls from_api on the typecaster). Raises
|
379
|
+
# TypecasterNotFound if no typecaster exists for this attribute
|
380
|
+
#
|
381
|
+
# @param attr_name [Symbol] The name of the attribute
|
382
|
+
# @param value [Object] The value to be typecasted
|
383
|
+
#
|
384
|
+
# @return [Object] The typecasted value
|
385
|
+
def typecast_attribute_for_read(attr_name, value)
|
386
|
+
self
|
387
|
+
.find_typecaster(attr_name)
|
388
|
+
.from_api(value)
|
389
|
+
end
|
390
|
+
|
391
|
+
#
|
392
|
+
# Returns the typecasted value of the attribute for being
|
393
|
+
# written (calls to_api on the typecaster). Raises
|
394
|
+
# TypecasterNotFound if no typecaster exists for this attribute
|
395
|
+
#
|
396
|
+
# @param attr_name [Symbol] The attribute in question
|
397
|
+
# @param value [Object] The value to be typecasted
|
398
|
+
#
|
399
|
+
# @return [Object] The typecasted value
|
400
|
+
def typecast_attribute_for_write(attr_name, value)
|
401
|
+
self
|
402
|
+
.find_typecaster(attr_name)
|
403
|
+
.to_api(value)
|
186
404
|
end
|
187
405
|
|
188
|
-
|
189
|
-
|
406
|
+
#
|
407
|
+
# Returns a hash of attribute names as keys and typecasted values
|
408
|
+
# as hash values
|
409
|
+
#
|
410
|
+
# @return [HashWithIndifferentAccess] Map from attr name to value
|
411
|
+
def attributes
|
412
|
+
hash = HashWithIndifferentAccess.new
|
413
|
+
|
414
|
+
self.class.attribute_names.each_with_object(hash) do |name, attrs|
|
415
|
+
attrs[name] = read_attribute(name)
|
416
|
+
end
|
190
417
|
end
|
191
418
|
|
192
|
-
|
193
|
-
|
194
|
-
|
419
|
+
#
|
420
|
+
# Handles mass assignment of attributes, including sanitizing them
|
421
|
+
# for mass assignment. Which by default does nothing but would if you
|
422
|
+
# were to use this in rails 4 or with strong_parameters
|
423
|
+
#
|
424
|
+
# @param attrs [Hash] Hash of attributes to mass assign
|
425
|
+
#
|
426
|
+
# @return [Hash] The passed in attrs param (with keys symbolized)
|
427
|
+
def attributes=(attrs)
|
428
|
+
unless attrs.respond_to?(:symbolize_keys)
|
429
|
+
raise ArgumentError, 'You must pass a hash when assigning attributes'
|
430
|
+
end
|
195
431
|
|
196
|
-
|
197
|
-
|
432
|
+
return if attrs.blank?
|
433
|
+
|
434
|
+
attrs = attrs.symbolize_keys
|
435
|
+
# First deal with sanitizing for mass assignment
|
436
|
+
# this raises an error if attrs violates mass assignment rules
|
437
|
+
attrs = self.sanitize_for_mass_assignment(attrs)
|
438
|
+
|
439
|
+
attrs.each do |name, value|
|
440
|
+
self._assign_attribute(name, value)
|
198
441
|
end
|
199
|
-
|
200
|
-
|
201
|
-
@attributes_cache.delete(name.to_sym)
|
202
|
-
@attributes[name.to_sym] = @attributes_cache[name.to_sym] = new_val
|
442
|
+
|
443
|
+
attrs
|
203
444
|
end
|
204
|
-
|
445
|
+
|
446
|
+
#
|
447
|
+
# Reads an attribute and raises MissingAttributeError
|
448
|
+
#
|
449
|
+
# @param attr_name [Symbol] The attribute to read
|
450
|
+
#
|
451
|
+
# @return [Object] The value of the attribute
|
452
|
+
def [](attr_name)
|
453
|
+
read_attribute(attr_name) do |n|
|
454
|
+
self.missing_attribute(attr_name, caller(0))
|
455
|
+
end
|
456
|
+
end
|
457
|
+
|
458
|
+
#
|
459
|
+
# Write the value to the given attribute
|
460
|
+
# @param attr_name [Symbol] The attribute to write
|
461
|
+
# @param value [Object] The value to be written
|
462
|
+
#
|
463
|
+
# @return [Object] Returns the value parameter
|
464
|
+
def []=(attr_name, value)
|
465
|
+
write_attribute(attr_name, value)
|
466
|
+
end
|
467
|
+
|
468
|
+
#
|
469
|
+
# Wrapper for the class method attribute? that
|
470
|
+
# returns true if the name parameter is the name of
|
471
|
+
# any attribute
|
472
|
+
#
|
473
|
+
# @param name [Symbol] The name to query
|
474
|
+
#
|
475
|
+
# @return [Boolean] True if the name param is the name
|
476
|
+
# of an attribute
|
205
477
|
def attribute?(name)
|
206
478
|
self.class.attribute?(name)
|
207
479
|
end
|
208
|
-
|
480
|
+
|
481
|
+
#
|
482
|
+
# Wrapper for the class method protected_attribute?
|
483
|
+
#
|
484
|
+
# @param name [Symbol] The name to query
|
485
|
+
#
|
486
|
+
# @return [Boolean] True if name is a protected attribute
|
209
487
|
def protected_attribute?(name)
|
210
488
|
self.class.protected_attribute?(name)
|
211
489
|
end
|
212
|
-
|
490
|
+
|
491
|
+
#
|
492
|
+
# Wrapper for the class method public_attribute?
|
493
|
+
#
|
494
|
+
# @param name [Symbol] The name to query
|
495
|
+
#
|
496
|
+
# @return [Boolean] True if name is a public attribute
|
497
|
+
def public_attribute?(name)
|
498
|
+
self.class.public_attribute?(name)
|
499
|
+
end
|
500
|
+
|
501
|
+
#
|
502
|
+
# Override for the save method to update our dirty tracking
|
503
|
+
# of attributes
|
504
|
+
#
|
505
|
+
# @param *args [Array] Used to clear changes on any associations
|
506
|
+
# embedded in this save provided it succeeds
|
507
|
+
#
|
508
|
+
# @return [Boolean] True if the save succeeded, false otherwise
|
509
|
+
def save_with_dirty_tracking(*args)
|
510
|
+
if save_without_dirty_tracking(*args)
|
511
|
+
self.make_changes_current
|
512
|
+
if args.first.is_a?(Hash) && args.first[:include_associations]
|
513
|
+
args.first[:include_associations].each do |assoc|
|
514
|
+
Array.wrap(self.send(assoc).internal_object).each(&:make_changes_current)
|
515
|
+
end
|
516
|
+
end
|
517
|
+
return true
|
518
|
+
else
|
519
|
+
return false
|
520
|
+
end
|
521
|
+
end
|
522
|
+
|
523
|
+
#
|
524
|
+
# Override to respond_to? for finding attribute methods even
|
525
|
+
# if they are not defined
|
526
|
+
#
|
527
|
+
# @param sym [Symbol] The method that we may respond to
|
528
|
+
# @param include_private_methods = false [Boolean] Whether or not
|
529
|
+
# we should consider private methods
|
530
|
+
#
|
531
|
+
# @return [Boolean] True if we respond to sym
|
213
532
|
def respond_to?(sym, include_private_methods = false)
|
214
533
|
if sym =~ /\?$/
|
215
534
|
return true if self.attribute?($`)
|
@@ -220,65 +539,145 @@ module ApiResource
|
|
220
539
|
end
|
221
540
|
super
|
222
541
|
end
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
when :array
|
229
|
-
return []
|
230
|
-
else
|
231
|
-
return nil
|
542
|
+
|
543
|
+
def method_missing(sym, *args, &block)
|
544
|
+
sym = sym.to_sym
|
545
|
+
if @attributes.keys.symbolize_array.include?(sym)
|
546
|
+
return @attributes[sym]
|
232
547
|
end
|
548
|
+
super
|
233
549
|
end
|
234
550
|
|
235
|
-
def
|
551
|
+
def make_changes_current
|
552
|
+
@previously_changed = self.changes
|
553
|
+
@changed_attributes.clear
|
554
|
+
end
|
236
555
|
|
237
|
-
|
238
|
-
@
|
556
|
+
def clear_changes(*attrs)
|
557
|
+
@previously_changed = {}
|
558
|
+
@changed_attributes = {}
|
559
|
+
true
|
560
|
+
end
|
239
561
|
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
val = @attributes[field.to_sym]
|
246
|
-
else
|
247
|
-
val = self.default_value_for_field(field)
|
248
|
-
end
|
249
|
-
# now we typecast
|
250
|
-
val = val.nil? ? nil : self.typecast_attribute(field, val)
|
251
|
-
return @attributes_cache[field.to_sym] = val
|
562
|
+
def reset_changes
|
563
|
+
self.class.attribute_names.each do |attr_name|
|
564
|
+
attr_name = attr_name.to_sym
|
565
|
+
|
566
|
+
reset_attribute!(attr_name)
|
252
567
|
end
|
568
|
+
true
|
253
569
|
end
|
254
570
|
|
255
|
-
|
256
|
-
#
|
257
|
-
#
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
571
|
+
protected
|
572
|
+
#
|
573
|
+
# Default implementation that would be overridden by including
|
574
|
+
# the behavior from strong parameters
|
575
|
+
#
|
576
|
+
# @param attrs [Hash] Attributes to sanitize (by default do nothing)
|
577
|
+
#
|
578
|
+
# @return [Hash] Unmodified attrs
|
579
|
+
def sanitize_for_mass_assignment(attrs)
|
580
|
+
return attrs
|
581
|
+
end
|
582
|
+
|
583
|
+
#
|
584
|
+
# Writes an attribute by proxying to the writer methods.
|
585
|
+
# Raises MissingAttributeError if #{name}= does not exist
|
586
|
+
#
|
587
|
+
# @param name [Symbol] The attribute to write
|
588
|
+
# @param value [Object] The value to write
|
589
|
+
#
|
590
|
+
# @return [Boolean] Always true
|
591
|
+
def _assign_attribute(name, value)
|
592
|
+
# special case if we are assigning a protected attribute
|
593
|
+
# since it has no writer method
|
594
|
+
if self.protected_attribute?(name)
|
595
|
+
return self.write_attribute(name, value)
|
596
|
+
end
|
597
|
+
|
598
|
+
begin
|
599
|
+
# Call the method only if it is public
|
600
|
+
self.public_send("#{name}=", value)
|
601
|
+
rescue NoMethodError
|
602
|
+
# If we get a no method error we should re-raise it
|
603
|
+
# if it wasn't because #{name}= is not defined
|
604
|
+
if self.respond_to?("#{name}=")
|
605
|
+
raise
|
606
|
+
else
|
607
|
+
# Otherwise we raise MissingAttributeError
|
608
|
+
self.missing_attribute(name, caller(0))
|
609
|
+
end
|
262
610
|
end
|
263
611
|
end
|
264
612
|
|
265
|
-
|
266
|
-
|
613
|
+
#
|
614
|
+
# Searches for the typecaster for the given attribute name
|
615
|
+
# raising ApiResource::TypecasterNotFound if it
|
616
|
+
# cannot find one
|
617
|
+
#
|
618
|
+
# @param attr_name [Symbol] The attribute whose typecaster you're after
|
619
|
+
#
|
620
|
+
# @return [ApiResource::Typecaster] An object for typecasting attribute
|
621
|
+
# values
|
622
|
+
def find_typecaster(attr_name)
|
623
|
+
attr_name = attr_name.to_sym
|
624
|
+
|
625
|
+
typecaster = self.class.attribute_types[attr_name]
|
626
|
+
|
627
|
+
if typecaster.nil?
|
628
|
+
typecaster = ApiResource::Typecast::UnknownTypecaster
|
629
|
+
end
|
630
|
+
|
631
|
+
return typecaster
|
632
|
+
end
|
633
|
+
|
634
|
+
#
|
635
|
+
# Helper for raising a MissingAttributeError
|
636
|
+
#
|
637
|
+
# @param name [Symbol] The missing attribute's name
|
638
|
+
# @param backtrace [Object] The backtrace of where the
|
639
|
+
# error occurred
|
640
|
+
#
|
641
|
+
# @return [type] [description]
|
642
|
+
def missing_attribute(name, backtrace)
|
643
|
+
raise ActiveModel::MissingAttributeError.new(
|
644
|
+
"could not find attribute #{name}",
|
645
|
+
backtrace
|
646
|
+
)
|
647
|
+
end
|
648
|
+
|
649
|
+
def clone_attribute_value(meth, attr_name)
|
650
|
+
attr_name = attr_name.to_sym
|
651
|
+
|
652
|
+
result = self.send(meth, attr_name)
|
653
|
+
|
654
|
+
return result.duplicable? ? result.clone : result
|
655
|
+
end
|
656
|
+
|
267
657
|
|
268
658
|
private
|
269
659
|
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
660
|
+
# this is here for compatibility with ActiveModel::AttributeMethods
|
661
|
+
# it is the fallback called in method_missing
|
662
|
+
#
|
663
|
+
# @param name [Symbol] The attribute to read
|
664
|
+
#
|
665
|
+
# @return [Object] The value read
|
666
|
+
def attribute(name)
|
667
|
+
read_attribute(name)
|
668
|
+
end
|
669
|
+
|
670
|
+
# this is here for compatibility with ActiveModel::AttributeMethods
|
671
|
+
# it is the fallback called in method_missing
|
672
|
+
#
|
673
|
+
# @param name [Symbol] The attribute to
|
674
|
+
# @param val [Object] The value to assign
|
675
|
+
#
|
676
|
+
# @return [Object] val
|
677
|
+
def attribute=(name, val)
|
678
|
+
write_attribute(name, val)
|
679
|
+
end
|
275
680
|
|
276
|
-
# this is here for compatibility with ActiveModel::AttributeMethods
|
277
|
-
# it is the fallback called in method_missing
|
278
|
-
def attribute=(name, val)
|
279
|
-
write_attribute(name, val)
|
280
|
-
end
|
281
|
-
|
282
681
|
end
|
283
|
-
|
682
|
+
|
284
683
|
end
|