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.
@@ -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