api_resource 0.4.3 → 0.5.0

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