api_resource 0.4.3 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (89) hide show
  1. data/VERSION +1 -1
  2. data/api_resource.gemspec +4 -74
  3. data/coverage/assets/0.5.3/app.js +88 -0
  4. data/coverage/assets/0.5.3/fancybox/blank.gif +0 -0
  5. data/coverage/assets/0.5.3/fancybox/fancy_close.png +0 -0
  6. data/coverage/assets/0.5.3/fancybox/fancy_loading.png +0 -0
  7. data/coverage/assets/0.5.3/fancybox/fancy_nav_left.png +0 -0
  8. data/coverage/assets/0.5.3/fancybox/fancy_nav_right.png +0 -0
  9. data/coverage/assets/0.5.3/fancybox/fancy_shadow_e.png +0 -0
  10. data/coverage/assets/0.5.3/fancybox/fancy_shadow_n.png +0 -0
  11. data/coverage/assets/0.5.3/fancybox/fancy_shadow_ne.png +0 -0
  12. data/coverage/assets/0.5.3/fancybox/fancy_shadow_nw.png +0 -0
  13. data/coverage/assets/0.5.3/fancybox/fancy_shadow_s.png +0 -0
  14. data/coverage/assets/0.5.3/fancybox/fancy_shadow_se.png +0 -0
  15. data/coverage/assets/0.5.3/fancybox/fancy_shadow_sw.png +0 -0
  16. data/coverage/assets/0.5.3/fancybox/fancy_shadow_w.png +0 -0
  17. data/coverage/assets/0.5.3/fancybox/fancy_title_left.png +0 -0
  18. data/coverage/assets/0.5.3/fancybox/fancy_title_main.png +0 -0
  19. data/coverage/assets/0.5.3/fancybox/fancy_title_over.png +0 -0
  20. data/coverage/assets/0.5.3/fancybox/fancy_title_right.png +0 -0
  21. data/coverage/assets/0.5.3/fancybox/fancybox-x.png +0 -0
  22. data/coverage/assets/0.5.3/fancybox/fancybox-y.png +0 -0
  23. data/coverage/assets/0.5.3/fancybox/fancybox.png +0 -0
  24. data/coverage/assets/0.5.3/fancybox/jquery.fancybox-1.3.1.css +363 -0
  25. data/coverage/assets/0.5.3/fancybox/jquery.fancybox-1.3.1.pack.js +44 -0
  26. data/coverage/assets/0.5.3/favicon_green.png +0 -0
  27. data/coverage/assets/0.5.3/favicon_red.png +0 -0
  28. data/coverage/assets/0.5.3/favicon_yellow.png +0 -0
  29. data/coverage/assets/0.5.3/highlight.css +129 -0
  30. data/coverage/assets/0.5.3/highlight.pack.js +1 -0
  31. data/coverage/assets/0.5.3/jquery-1.6.2.min.js +18 -0
  32. data/coverage/assets/0.5.3/jquery.dataTables.min.js +152 -0
  33. data/coverage/assets/0.5.3/jquery.timeago.js +141 -0
  34. data/coverage/assets/0.5.3/jquery.url.js +174 -0
  35. data/coverage/assets/0.5.3/loading.gif +0 -0
  36. data/coverage/assets/0.5.3/magnify.png +0 -0
  37. data/coverage/assets/0.5.3/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
  38. data/coverage/assets/0.5.3/smoothness/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
  39. data/coverage/assets/0.5.3/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
  40. data/coverage/assets/0.5.3/smoothness/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
  41. data/coverage/assets/0.5.3/smoothness/images/ui-bg_glass_75_dadada_1x400.png +0 -0
  42. data/coverage/assets/0.5.3/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
  43. data/coverage/assets/0.5.3/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
  44. data/coverage/assets/0.5.3/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
  45. data/coverage/assets/0.5.3/smoothness/images/ui-icons_222222_256x240.png +0 -0
  46. data/coverage/assets/0.5.3/smoothness/images/ui-icons_2e83ff_256x240.png +0 -0
  47. data/coverage/assets/0.5.3/smoothness/images/ui-icons_454545_256x240.png +0 -0
  48. data/coverage/assets/0.5.3/smoothness/images/ui-icons_888888_256x240.png +0 -0
  49. data/coverage/assets/0.5.3/smoothness/images/ui-icons_cd0a0a_256x240.png +0 -0
  50. data/coverage/assets/0.5.3/smoothness/jquery-ui-1.8.4.custom.css +295 -0
  51. data/coverage/assets/0.5.3/stylesheet.css +383 -0
  52. data/coverage/index.html +3573 -0
  53. data/lib/api_resource/associations/abstract_scope.rb +191 -0
  54. data/lib/api_resource/associations/association_scope.rb +47 -0
  55. data/lib/api_resource/associations/belongs_to_remote_object_proxy.rb +5 -6
  56. data/lib/api_resource/associations/has_many_remote_object_proxy.rb +5 -8
  57. data/lib/api_resource/associations/has_one_remote_object_proxy.rb +12 -13
  58. data/lib/api_resource/associations/multi_object_proxy.rb +65 -39
  59. data/lib/api_resource/associations/resource_scope.rb +6 -17
  60. data/lib/api_resource/associations/scope.rb +23 -121
  61. data/lib/api_resource/associations/single_object_proxy.rb +41 -50
  62. data/lib/api_resource/associations.rb +32 -11
  63. data/lib/api_resource/attributes.rb +108 -69
  64. data/lib/api_resource/base.rb +114 -106
  65. data/lib/api_resource/local.rb +1 -1
  66. data/lib/api_resource/model_errors.rb +9 -6
  67. data/lib/api_resource/scopes.rb +53 -16
  68. data/lib/api_resource.rb +3 -1
  69. data/spec/lib/api_resource_spec.rb +3 -7
  70. data/spec/lib/associations/association_scope_spec.rb +19 -0
  71. data/spec/lib/associations_spec.rb +251 -162
  72. data/spec/lib/attributes_spec.rb +33 -15
  73. data/spec/lib/base_spec.rb +302 -64
  74. data/spec/lib/callbacks_spec.rb +4 -2
  75. data/spec/lib/local_spec.rb +5 -1
  76. data/spec/spec_helper.rb +2 -3
  77. data/spec/support/mocks/association_mocks.rb +9 -1
  78. data/spec/support/requests/association_requests.rb +5 -5
  79. data/spec/support/requests/test_resource_requests.rb +16 -4
  80. data/spec/tmp/api_resource_test_db.sqlite +0 -0
  81. metadata +68 -22
  82. data/.document +0 -5
  83. data/.rspec +0 -5
  84. data/.travis.yml +0 -4
  85. data/lib/api_resource/associations/association_proxy.rb +0 -121
  86. data/lib/api_resource/associations/dynamic_resource_scope.rb +0 -23
  87. data/lib/api_resource/associations/generic_scope.rb +0 -68
  88. data/lib/api_resource/associations/multi_argument_resource_scope.rb +0 -15
  89. data/lib/api_resource/associations/relation_scope.rb +0 -25
@@ -1,23 +1,45 @@
1
- require 'api_resource/associations/association_proxy'
1
+ require 'api_resource/associations/association_scope'
2
2
 
3
3
  module ApiResource
4
4
 
5
5
  module Associations
6
6
 
7
- class SingleObjectProxy < AssociationProxy
7
+ class SingleObjectProxy < AssociationScope
8
8
 
9
9
  def serializable_hash(options = {})
10
10
  return if self.internal_object.nil?
11
11
  self.internal_object.serializable_hash(options)
12
12
  end
13
+
14
+ def internal_object
15
+ unless instance_variable_defined?(:@internal_object)
16
+ if self.remote_path.present?
17
+ instance_variable_set(:@internal_object, self.load)
18
+ else
19
+ instance_variable_set(:@internal_object, nil)
20
+ end
21
+ end
22
+ instance_variable_get(:@internal_object)
23
+ end
13
24
 
14
25
  def internal_object=(contents)
15
- if contents.is_a?(self.klass) ||
16
- contents.respond_to?(:internal_object) && contents.internal_object.is_a?(self.klass) ||
17
- contents.nil?
26
+ if contents.is_a?(self.klass) || contents.nil?
18
27
  return @internal_object = contents
28
+ elsif contents.is_a?(self.class)
29
+ return @internal_object = contents.internal_object
30
+ # a Hash may be attributes and/or a service_uri
31
+ elsif contents.is_a?(Hash)
32
+ contents = contents.symbolize_keys
33
+ @remote_path = contents.delete(
34
+ self.class.remote_path_element.to_sym
35
+ )
36
+ if contents.present?
37
+ return @internal_object = self.klass.instantiate_record(contents)
38
+ end
19
39
  else
20
- return load(contents)
40
+ raise ArgumentError.new(
41
+ "#{contents} must be a #{self.klass}, a #{self.class} or a Hash"
42
+ )
21
43
  end
22
44
  end
23
45
 
@@ -27,53 +49,22 @@ module ApiResource
27
49
  return true
28
50
  end
29
51
 
30
- protected
31
- def load_scope_with_options(scope, options)
32
- scope = self.loaded_hash_key(scope.to_s, options)
33
- # If the service uri is blank you can't load
34
- return nil if self.remote_path.blank?
35
- self.loaded[scope] ||= begin
36
- self.times_loaded += 1
37
- self.klass.new(self.load_from_remote(options))
38
- end
52
+ def hash
53
+ self.id.hash
39
54
  end
40
55
 
41
- def load(contents)
42
- # If we get something nil this should just behave like nil
43
- return if contents.nil?
44
- # if we get an array with a length of one, make it a hash
45
- if contents.is_a?(self.class)
46
- contents = contents.internal_object.serializable_hash
47
- elsif contents.is_a?(Array) && contents.length == 1
48
- contents = contents.first
49
- end
50
- raise "Expected an attributes hash got #{contents}" unless contents.is_a?(Hash)
51
- contents = contents.with_indifferent_access
52
- # If we don't have a 'service_uri' just assume that these are all attributes and make an object
53
- return @internal_object = self.klass.new(contents) unless contents[self.class.remote_path_element]
54
- # allow for symbols vs strings with these elements
55
- self.remote_path = contents.delete(self.class.remote_path_element)
56
- # There's only one hash here so it's hard to distinguish attributes from scopes, the key scopes_only says everything
57
- # in this hash is a scope
58
- no_attrs = (contents.delete(:scopes_only) || false)
59
- attrs = {}
60
- contents.each do |key, val|
61
- # if this key is an attribute add it to attrs, warn if we've set scopes_only
62
- if self.klass.attribute_names.include?(key.to_sym) && !no_attrs
63
- attrs[key] = val
64
- else
65
- warn("#{key} is an attribute of #{self.klass}, beware of name collisions") if no_attrs && self.klass.attribute_names.include?(key)
66
- raise "Expected the scope #{key} to have a hash for a value, got #{val}" unless val.is_a?(Hash)
67
- self.instance_eval <<-EOE, __FILE__, __LINE__ + 1
68
- def #{key}(opts = {})
69
- @#{key} ||= ApiResource::Associations::RelationScope.new(self, :#{key}, opts)
70
- end
71
- EOE
72
- self.scopes[key.to_s] = val
73
- end
74
- end
75
- @internal_object = attrs.present? ? self.klass.new(attrs) : nil
56
+ def eql?(other)
57
+ return self == other
76
58
  end
59
+
60
+
61
+ def load(opts = {})
62
+ data = self.klass.connection.get(self.build_load_path(opts))
63
+ @loaded = true
64
+ return nil if data.blank?
65
+ return self.klass.instantiate_record(data)
66
+ end
67
+
77
68
 
78
69
  end
79
70
 
@@ -1,13 +1,12 @@
1
1
  require 'active_support'
2
2
  require 'active_support/string_inquirer'
3
3
  require 'api_resource/association_activation'
4
- require 'api_resource/associations/relation_scope'
4
+ require 'api_resource/associations/abstract_scope'
5
+ require 'api_resource/associations/scope'
5
6
  require 'api_resource/associations/resource_scope'
6
- require 'api_resource/associations/dynamic_resource_scope'
7
- require 'api_resource/associations/generic_scope'
8
- require 'api_resource/associations/multi_argument_resource_scope'
9
- require 'api_resource/associations/multi_object_proxy'
7
+ require 'api_resource/associations/association_scope'
10
8
  require 'api_resource/associations/single_object_proxy'
9
+ require 'api_resource/associations/multi_object_proxy'
11
10
  require 'api_resource/associations/belongs_to_remote_object_proxy'
12
11
  require 'api_resource/associations/has_one_remote_object_proxy'
13
12
  require 'api_resource/associations/has_many_remote_object_proxy'
@@ -129,6 +128,10 @@ module ApiResource
129
128
  # structure is {:has_many => {"myname" => "ClassName"}}
130
129
  self.related_objects.clone.delete_if{|k,v| k.to_s == "scopes"}.collect{|k,v| v.keys.collect(&:to_sym)}.flatten
131
130
  end
131
+
132
+ def association_class(assoc)
133
+ self.association_class_name(assoc).constantize
134
+ end
132
135
 
133
136
  def association_class_name(assoc)
134
137
  raise ArgumentError, "#{assoc} is not a valid association of #{self}" unless self.association?(assoc)
@@ -162,18 +165,32 @@ module ApiResource
162
165
  end
163
166
 
164
167
  def define_association_as_attribute(assoc_type, assoc_name)
165
- # set up dirty tracking for associations
166
-
168
+ # set up dirty tracking for associations, but only for ApiResource
169
+ # these methods are also used for ActiveRecord
170
+ # TODO: change this
171
+ if self.ancestors.include?(ApiResource::Base)
172
+ define_attribute_method(assoc_name)
173
+ end
174
+
167
175
  self.class_eval <<-EOE, __FILE__, __LINE__ + 1
168
176
  def #{assoc_name}
169
- self.assoc_attributes[:#{assoc_name}] ||= (self.attributes[:#{assoc_name}] || #{self.association_types[assoc_type.to_sym].to_s.classify}ObjectProxy.new(self.association_class_name('#{assoc_name}'), nil, self))
177
+ @attributes_cache[:#{assoc_name}] ||= begin
178
+ klass = #{self.association_types[assoc_type.to_sym].to_s.classify}ObjectProxy
179
+ instance = klass.new(
180
+ self.association_class('#{assoc_name}'), self
181
+ )
182
+ if @attributes[:#{assoc_name}].present?
183
+ instance.internal_object = @attributes[:#{assoc_name}]
184
+ end
185
+ instance
186
+ end
170
187
  end
171
188
  def #{assoc_name}=(val)
172
189
  # get old internal object
173
- old_internal_object = self.#{assoc_name}.internal_object
190
+ unless self.#{assoc_name}.internal_object == val
191
+ #{assoc_name}_will_change!
192
+ end
174
193
  self.#{assoc_name}.internal_object = val
175
- #{assoc_name}_will_change! unless self.#{assoc_name} == old_internal_object
176
- self.#{assoc_name}.internal_object
177
194
  end
178
195
  def #{assoc_name}?
179
196
  self.#{assoc_name}.internal_object.present?
@@ -204,6 +221,10 @@ module ApiResource
204
221
  self.class.association?(assoc)
205
222
  end
206
223
 
224
+ def association_class(assoc)
225
+ self.class.association_class(assoc)
226
+ end
227
+
207
228
  def association_class_name(assoc)
208
229
  self.class.association_class_name(assoc)
209
230
  end
@@ -10,27 +10,20 @@ module ApiResource
10
10
 
11
11
  alias_method_chain :save, :dirty_tracking
12
12
 
13
- class_attribute :attribute_names, :public_attribute_names, :protected_attribute_names, :attribute_types
13
+ class_attribute(
14
+ :attribute_names,
15
+ :public_attribute_names,
16
+ :protected_attribute_names,
17
+ :attribute_types
18
+ )
14
19
 
15
20
  cattr_accessor :valid_typecasts; self.valid_typecasts = [:date, :time, :float, :integer, :int, :fixnum, :string, :array]
16
21
 
17
- attr_reader :attributes
18
-
19
22
  self.attribute_names = []
20
23
  self.public_attribute_names = []
21
24
  self.protected_attribute_names = []
22
25
  self.attribute_types = {}.with_indifferent_access
23
26
 
24
- define_method(:attributes) do
25
- return @attributes if @attributes
26
- # Otherwise make the attributes hash of all the attributes
27
- @attributes = HashWithIndifferentAccess.new
28
- self.class.attribute_names.each do |attr|
29
- @attributes[attr] = self.send("#{attr}")
30
- end
31
- @attributes
32
- end
33
-
34
27
 
35
28
  # This method is important for reloading an object. If the
36
29
  # object has already been loaded, its associations will trip
@@ -68,6 +61,7 @@ module ApiResource
68
61
  module ClassMethods
69
62
 
70
63
  def define_attributes(*args)
64
+
71
65
  args.each do |arg|
72
66
  if arg.is_a?(Array)
73
67
  self.define_attribute_type(arg.first, arg.second)
@@ -75,24 +69,9 @@ module ApiResource
75
69
  end
76
70
  self.attribute_names += [arg.to_sym]
77
71
  self.public_attribute_names += [arg.to_sym]
78
-
79
- # Override the setter for dirty tracking
80
- self.class_eval <<-EOE, __FILE__, __LINE__ + 1
81
- def #{arg}
82
- attribute_with_default(:#{arg})
83
- end
84
-
85
- def #{arg}=(val)
86
- real_val = typecast_attribute(:#{arg}, val)
87
- #{arg}_will_change! unless self.#{arg} == real_val
88
- attributes[:#{arg}] = real_val
89
- end
90
-
91
- def #{arg}?
92
- attributes[:#{arg}].present?
93
- end
94
- EOE
72
+ self.define_accessor_methods(arg)
95
73
  end
74
+
96
75
  self.attribute_names.uniq!
97
76
  self.public_attribute_names.uniq!
98
77
  end
@@ -108,28 +87,35 @@ module ApiResource
108
87
  self.attribute_names += [arg.to_sym]
109
88
  self.protected_attribute_names += [arg.to_sym]
110
89
 
111
- # These attributes cannot be set, throw an error if you try
112
- self.class_eval <<-EOE, __FILE__, __LINE__ + 1
113
-
114
- def #{arg}
115
- self.attribute_with_default(:#{arg})
116
- end
117
-
118
- def #{arg}=(val)
119
- raise "#{arg} is a protected attribute and cannot be set"
120
- end
121
-
122
- def #{arg}?
123
- self.attributes[:#{arg}].present?
124
- end
125
- EOE
90
+ self.define_accessor_methods(arg)
126
91
  end
127
92
  self.attribute_names.uniq!
128
93
  self.protected_attribute_names.uniq!
129
94
  end
130
95
 
96
+ def define_accessor_methods(meth)
97
+ # Override the setter for dirty tracking
98
+ self.class_eval <<-EOE, __FILE__, __LINE__ + 1
99
+ def #{meth}
100
+ read_attribute(:#{meth})
101
+ end
102
+
103
+ def #{meth}=(new_val)
104
+ write_attribute(:#{meth}, new_val)
105
+ end
106
+
107
+ def #{meth}?
108
+ read_attribute(:#{meth}).present?
109
+ end
110
+ EOE
111
+ # sets up dirty tracking
112
+ define_attribute_method(meth)
113
+ end
114
+
131
115
  def define_attribute_type(field, type)
132
- raise "#{type} is not a valid type" unless self.valid_typecasts.include?(type.to_sym)
116
+ unless self.valid_typecasts.include?(type.to_sym)
117
+ raise "#{type} is not a valid type"
118
+ end
133
119
  self.attribute_types[field] = type.to_sym
134
120
  end
135
121
 
@@ -148,15 +134,31 @@ module ApiResource
148
134
  self.protected_attribute_names.clear
149
135
  end
150
136
  end
151
-
137
+
138
+ # override the initializer to set up some default values
139
+ def initialize(*args)
140
+ @attributes = @attributes_cache = HashWithIndifferentAccess.new
141
+ end
142
+
143
+ def attributes
144
+ attrs = {}
145
+ self.attribute_names.each{|name| attrs[name] = read_attribute(name)}
146
+ attrs
147
+ end
148
+
152
149
  # set new attributes
153
150
  def attributes=(new_attrs)
154
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
+ )
156
+ end
155
157
  self.send("#{k}=",v) unless k.to_sym == :id
156
158
  end
157
159
  new_attrs
158
160
  end
159
-
161
+
160
162
  def save_with_dirty_tracking(*args)
161
163
  if save_without_dirty_tracking(*args)
162
164
  @previously_changed = self.changes
@@ -182,6 +184,23 @@ module ApiResource
182
184
 
183
185
  set_attributes_as_current(*attrs)
184
186
  end
187
+
188
+ def read_attribute(name)
189
+ self.typecasted_attribute(name.to_sym)
190
+ end
191
+
192
+ def write_attribute(name, val)
193
+ old_val = read_attribute(name)
194
+ new_val = self.typecast_attribute(name, val)
195
+
196
+ unless old_val == new_val
197
+ self.send("#{name}_will_change!")
198
+ 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
203
+ end
185
204
 
186
205
  def attribute?(name)
187
206
  self.class.attribute?(name)
@@ -204,10 +223,6 @@ module ApiResource
204
223
 
205
224
  protected
206
225
 
207
- def attribute_with_default(field)
208
- self.attributes[field].nil? ? self.default_value_for_field(field) : self.attributes[field]
209
- end
210
-
211
226
  def default_value_for_field(field)
212
227
  case self.class.attribute_types[field.to_sym]
213
228
  when :array
@@ -217,25 +232,49 @@ module ApiResource
217
232
  end
218
233
  end
219
234
 
220
- def typecast_attribute(field, val)
221
- return val unless self.class.attribute_types.include?(field)
222
- case self.class.attribute_types[field.to_sym]
223
- when :date
224
- return val.class == Date ? val.dup : Date.parse(val)
225
- when :time
226
- return val.class == Time ? val.dup : Time.parse(val)
227
- when :integer, :int, :fixnum
228
- return val.class == Fixnum ? val.dup : val.to_i rescue val
229
- when :float
230
- return val.class == Float ? val.dup : val.to_f rescue val
231
- when :string
232
- return val.class == String ? val.dup : val.to_s rescue val
233
- when :array
234
- return val.class == Array ? val.dup : Array.wrap(val)
235
+ def typecasted_attribute(field)
236
+
237
+ @attributes ||= HashWithIndifferentAccess.new
238
+ @attributes_cache ||= HashWithIndifferentAccess.new
239
+
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]
235
246
  else
236
- # catches the nil case and just leaves it alone
237
- return val.dup rescue val
247
+ val = self.default_value_for_field(field)
248
+ end
249
+ # now we typecast
250
+ val = self.typecast_attribute(field, val)
251
+ return @attributes_cache[field.to_sym] = val
252
+ end
253
+ end
254
+
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) && val.present?
259
+ val = case self.class.attribute_types[field.to_sym]
260
+ when :date
261
+ val.class == Date ? val.dup : Date.parse(val)
262
+ when :time
263
+ val.class == Time ? val.dup : Time.parse(val)
264
+ when :integer, :int, :fixnum
265
+ val.class == Fixnum ? val.dup : val.to_i rescue val
266
+ when :float
267
+ val.class == Float ? val.dup : val.to_f rescue val
268
+ when :string
269
+ val.class == String ? val.dup : val.to_s rescue val
270
+ when :array
271
+ val.class == Array ? val.dup : Array.wrap(val)
272
+ else
273
+ # catches the nil case and just leaves it alone
274
+ val.dup rescue val
275
+ end
238
276
  end
277
+ val
239
278
  end
240
279
 
241
280
  end