api_resource 0.6.18 → 0.6.19

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- def define_attributes(*args)
65
- args.each do |arg|
66
- self.store_attribute_data(arg, :public)
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
- self.attribute_names.uniq!
69
- self.public_attribute_names.uniq!
106
+ true
70
107
  end
71
-
72
- def define_protected_attributes(*args)
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.store_attribute_data(arg, :protected)
123
+ self.initialize_attribute(
124
+ Array.wrap(arg),
125
+ options[:access_level]
126
+ )
75
127
  end
76
- self.attribute_names.uniq!
77
- self.protected_attribute_names.uniq!
128
+
129
+ self.define_attribute_methods(
130
+ args,
131
+ options[:access_level]
132
+ )
78
133
  end
79
134
 
80
- def define_accessor_methods(meth)
81
- # Override the setter for dirty tracking
82
- self.class_eval <<-EOE, __FILE__, __LINE__ + 1
83
- def #{meth}
84
- read_attribute(:#{meth})
85
- end
86
-
87
- def #{meth}=(new_val)
88
- write_attribute(:#{meth}, new_val)
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
- def #{meth}?
92
- read_attribute(:#{meth}).present?
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
- EOE
95
- # sets up dirty tracking
96
- define_attribute_method(meth)
167
+
168
+ self.protected_attribute_names << attr_name
169
+ end
170
+ self.attribute_names << attr_name
171
+ true
97
172
  end
98
173
 
99
- def define_attribute_type(field, type)
100
- unless self.typecasters.keys.include?(type.to_sym)
101
- raise "#{type} is not a valid type"
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
- self.attribute_types = self.attribute_types.merge(field => type.to_sym)
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
- # stores the attribute type data and the name of the
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
- # override the initializer to set up some default values
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 = @attributes_cache = HashWithIndifferentAccess.new
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
- def attributes
144
- attrs = {}
145
- self.attribute_names.each{|name| attrs[name] = read_attribute(name)}
146
- attrs
147
- end
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
- # set new attributes
150
- def attributes=(new_attrs)
151
- new_attrs.each_pair do |k,v|
152
- if self.protected_attribute?(k)
153
- raise Exception.new(
154
- "#{k} is a protected attribute and cannot be mass-assigned"
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
- self.send("#{k}=",v) unless k.to_sym == :id
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
- def save_with_dirty_tracking(*args)
163
- if save_without_dirty_tracking(*args)
164
- @previously_changed = self.changes
165
- @changed_attributes.clear
166
- return true
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
- return false
352
+ old = clone_attribute_value(:read_attribute, attr_name)
353
+ changed_attributes[attr_name] = old if old != typed_value
169
354
  end
170
- end
171
-
172
- def set_attributes_as_current(*attrs)
173
- @changed_attributes.clear and return if attrs.blank?
174
- attrs.each do |attr|
175
- @changed_attributes.delete(attr.to_s)
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
- def reset_attribute_changes(*attrs)
180
- attrs = self.class.public_attribute_names if attrs.blank?
181
- attrs.each do |attr|
182
- self.send("reset_#{attr}!")
183
- end
184
-
185
- set_attributes_as_current(*attrs)
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
- def read_attribute(name)
189
- self.typecasted_attribute(name.to_sym)
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
- def write_attribute(name, val)
193
- old_val = read_attribute(name)
194
- new_val = val.nil? ? nil : self.typecast_attribute(name, val)
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
- unless old_val == new_val
197
- self.send("#{name}_will_change!")
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
- # delete the old cached value and assign new val to both
200
- # @attributes and @attributes_cache
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
- protected
225
-
226
- def default_value_for_field(field)
227
- case self.class.attribute_types[field.to_sym]
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 typecasted_attribute(field)
551
+ def make_changes_current
552
+ @previously_changed = self.changes
553
+ @changed_attributes.clear
554
+ end
236
555
 
237
- @attributes ||= HashWithIndifferentAccess.new
238
- @attributes_cache ||= HashWithIndifferentAccess.new
556
+ def clear_changes(*attrs)
557
+ @previously_changed = {}
558
+ @changed_attributes = {}
559
+ true
560
+ end
239
561
 
240
- if @attributes_cache.has_key?(field.to_sym)
241
- return @attributes_cache[field.to_sym]
242
- else
243
- # pull out of the raw attributes
244
- if @attributes.has_key?(field.to_sym)
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
- def typecast_attribute(field, val)
256
- # if we have a valid value and we are planning to typecast go
257
- # into this case statement
258
- if self.class.attribute_types.include?(field.to_sym)
259
- caster = self.class.typecasters[self.class.attribute_types[field.to_sym]]
260
- if caster.present?
261
- val = caster.from_api(val)
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
- return val
266
- end
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
- # this is here for compatibility with ActiveModel::AttributeMethods
271
- # it is the fallback called in method_missing
272
- def attribute(name)
273
- read_attribute(name)
274
- end
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