datamapper 0.2.4 → 0.2.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. data/CHANGELOG +16 -1
  2. data/README +10 -8
  3. data/environment.rb +1 -1
  4. data/example.rb +13 -5
  5. data/lib/data_mapper.rb +2 -0
  6. data/lib/data_mapper/adapters/abstract_adapter.rb +61 -0
  7. data/lib/data_mapper/adapters/data_object_adapter.rb +33 -6
  8. data/lib/data_mapper/adapters/mysql_adapter.rb +5 -0
  9. data/lib/data_mapper/adapters/postgresql_adapter.rb +12 -0
  10. data/lib/data_mapper/adapters/sql/commands/load_command.rb +6 -14
  11. data/lib/data_mapper/adapters/sql/mappings/column.rb +37 -26
  12. data/lib/data_mapper/adapters/sql/mappings/table.rb +50 -8
  13. data/lib/data_mapper/associations.rb +4 -5
  14. data/lib/data_mapper/associations/has_and_belongs_to_many_association.rb +2 -2
  15. data/lib/data_mapper/associations/has_many_association.rb +40 -13
  16. data/lib/data_mapper/associations/has_n_association.rb +1 -1
  17. data/lib/data_mapper/base.rb +5 -448
  18. data/lib/data_mapper/callbacks.rb +12 -2
  19. data/lib/data_mapper/context.rb +4 -0
  20. data/lib/data_mapper/database.rb +1 -1
  21. data/lib/data_mapper/identity_map.rb +2 -2
  22. data/lib/data_mapper/persistence.rb +538 -0
  23. data/lib/data_mapper/support/active_record_impersonation.rb +21 -3
  24. data/lib/data_mapper/support/errors.rb +2 -0
  25. data/lib/data_mapper/support/serialization.rb +7 -10
  26. data/lib/data_mapper/support/struct.rb +7 -0
  27. data/lib/data_mapper/validatable_extensions/validations/validates_uniqueness_of.rb +11 -4
  28. data/performance.rb +23 -10
  29. data/rakefile.rb +1 -1
  30. data/spec/active_record_impersonation_spec.rb +2 -6
  31. data/spec/acts_as_tree_spec.rb +3 -1
  32. data/spec/associations_spec.rb +40 -160
  33. data/spec/attributes_spec.rb +1 -1
  34. data/spec/base_spec.rb +41 -13
  35. data/spec/callbacks_spec.rb +32 -0
  36. data/spec/coersion_spec.rb +1 -1
  37. data/spec/column_spec.rb +22 -12
  38. data/spec/dependency_spec.rb +5 -3
  39. data/spec/embedded_value_spec.rb +33 -17
  40. data/spec/has_many_association_spec.rb +173 -0
  41. data/spec/legacy_spec.rb +2 -2
  42. data/spec/load_command_spec.rb +59 -7
  43. data/spec/models/animal.rb +6 -2
  44. data/spec/models/animals_exhibit.rb +3 -1
  45. data/spec/models/career.rb +2 -1
  46. data/spec/models/comment.rb +3 -1
  47. data/spec/models/exhibit.rb +3 -1
  48. data/spec/models/fruit.rb +3 -1
  49. data/spec/models/person.rb +10 -1
  50. data/spec/models/post.rb +3 -1
  51. data/spec/models/project.rb +3 -1
  52. data/spec/models/section.rb +3 -1
  53. data/spec/models/serializer.rb +3 -1
  54. data/spec/models/user.rb +3 -1
  55. data/spec/models/zoo.rb +3 -1
  56. data/spec/paranoia_spec.rb +3 -1
  57. data/spec/postgres_spec.rb +54 -0
  58. data/spec/save_command_spec.rb +9 -5
  59. data/spec/schema_spec.rb +0 -91
  60. data/spec/single_table_inheritance_spec.rb +8 -0
  61. data/spec/table_spec.rb +46 -0
  62. data/spec/validates_uniqueness_of_spec.rb +19 -1
  63. metadata +8 -10
  64. data/lib/data_mapper/associations/has_one_association.rb +0 -77
  65. data/plugins/dataobjects/do_rb +0 -0
@@ -60,7 +60,7 @@ module DataMapper
60
60
  def initialize
61
61
  @callbacks = Hash.new do |h,k|
62
62
  raise 'Callback names must be Symbols' unless k.kind_of?(Symbol)
63
- h[k] = []
63
+ h[k] = Set.new
64
64
  end
65
65
  end
66
66
 
@@ -89,9 +89,19 @@ module DataMapper
89
89
  # the instance executed against (as a method call).
90
90
  def add(name, block)
91
91
  callback = @callbacks[name]
92
- raise ArgumentError.new("You didn't specify a callback in String, Symbol or Proc form.") if block.nil?
92
+ raise ArgumentError.new("You didn't specify a callback in String, Symbol or Proc form.") unless [String, Proc, Symbol].detect { |type| block.is_a?(type) }
93
93
  callback << block
94
94
  end
95
+
96
+ def dup
97
+ copy = self.class.new
98
+ @callbacks.each_pair do |name, callbacks|
99
+ callbacks.each do |callback|
100
+ copy.add(name, callback)
101
+ end
102
+ end
103
+ return copy
104
+ end
95
105
  end
96
106
 
97
107
  end
@@ -44,6 +44,10 @@ module DataMapper
44
44
  @adapter.load(self, klass, options).first
45
45
  end
46
46
 
47
+ def get(klass, keys)
48
+ @adapter.get(self, klass, keys)
49
+ end
50
+
47
51
  def all(klass, options = {})
48
52
  @adapter.load(self, klass, options)
49
53
  end
@@ -168,7 +168,7 @@ module DataMapper
168
168
  @schema_search_path = nil
169
169
  @username = 'root'
170
170
  @password = ''
171
- @index_path = (Dir::pwd + "/indexes")
171
+ @socket = nil
172
172
 
173
173
  @log_level = Logger::WARN
174
174
  @log_stream = nil
@@ -38,8 +38,8 @@ module DataMapper
38
38
  end
39
39
 
40
40
  private
41
- def mapped_class(klass)
42
- if klass.superclass == DataMapper::Base
41
+ def mapped_class(klass)
42
+ if ! klass.superclass.respond_to?(:persistent?)
43
43
  klass
44
44
  else
45
45
  mapped_class(klass.superclass)
@@ -0,0 +1,538 @@
1
+ require 'data_mapper/property'
2
+ require 'data_mapper/support/active_record_impersonation'
3
+ require 'data_mapper/support/serialization'
4
+ require 'data_mapper/validations'
5
+ require 'data_mapper/associations'
6
+ require 'data_mapper/callbacks'
7
+ require 'data_mapper/embedded_value'
8
+ require 'data_mapper/auto_migrations'
9
+ require 'data_mapper/dependency_queue'
10
+ require 'data_mapper/support/struct'
11
+
12
+ module DataMapper
13
+ # See DataMapper::Persistence::ClassMethods for DataMapper's DSL documentation.
14
+ module Persistence
15
+ # This probably needs to be protected
16
+ attr_accessor :loaded_set
17
+
18
+ def self.included(klass)
19
+ klass.extend(ClassMethods)
20
+
21
+ klass.send(:include, Associations)
22
+ klass.send(:include, Validations)
23
+ klass.send(:include, CallbacksHelper)
24
+ klass.send(:include, Support::ActiveRecordImpersonation)
25
+ klass.send(:include, Support::Serialization)
26
+
27
+ prepare_for_persistence(klass)
28
+ end
29
+
30
+ def self.prepare_for_persistence(klass)
31
+ klass.instance_variable_set('@properties', [])
32
+
33
+ klass.send :extend, AutoMigrations
34
+ klass.subclasses
35
+ DataMapper::Persistence::subclasses << klass unless klass == DataMapper::Base
36
+ klass.send(:undef_method, :id) if method_defined?(:id)
37
+
38
+ return if klass == DataMapper::Base
39
+
40
+ # When this class is sub-classed, copy the declared columns.
41
+ klass.class_eval do
42
+ def self.subclasses
43
+ @subclasses || (@subclasses = [])
44
+ end
45
+
46
+ def self.inherited(subclass)
47
+ super_table = database.table(self)
48
+
49
+ if super_table.type_column.nil?
50
+ super_table.add_column(:type, :class, {})
51
+ end
52
+
53
+ subclass.instance_variable_set("@callbacks", self.callbacks.dup)
54
+
55
+ self::subclasses << subclass
56
+ end
57
+
58
+ def self.persistent?
59
+ true
60
+ end
61
+ end
62
+ end
63
+
64
+ def self.auto_migrate!
65
+ subclasses.each do |subclass|
66
+ subclass.auto_migrate!
67
+ end
68
+ end
69
+
70
+ # Track classes that include this module.
71
+ def self.subclasses
72
+ @subclasses || (@subclasses = [])
73
+ end
74
+
75
+ def self.dependencies
76
+ @dependency_queue || (@dependency_queue = DependencyQueue.new)
77
+ end
78
+
79
+ def initialize(details = nil)
80
+ case details
81
+ when Hash then self.attributes = details
82
+ when details.respond_to?(:persistent?) then self.unsafe_attributes = details.attributes
83
+ when Struct then self.unsafe_attributes = details.attributes
84
+ when NilClass then nil
85
+ end
86
+ end
87
+
88
+ module ClassMethods
89
+
90
+ # Track classes that include this module.
91
+ def subclasses
92
+ @subclasses || (@subclasses = [])
93
+ end
94
+
95
+ def logger
96
+ database.logger
97
+ end
98
+
99
+ def transaction
100
+ yield
101
+ end
102
+
103
+ def foreign_key
104
+ Inflector.underscore(self.name) + "_id"
105
+ end
106
+
107
+ def extended(klass)
108
+ unless klass == DataMapper::Base
109
+ klass.class_eval do
110
+ def persistent?
111
+ true
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+ def table
118
+ database.table(self)
119
+ end
120
+
121
+ # NOTE: check is only for psql, so maybe the postgres adapter should define
122
+ # its own property options. currently it will produce a warning tho since
123
+ # PROPERTY_OPTIONS is a constant
124
+ PROPERTY_OPTIONS = [
125
+ :public, :protected, :private, :accessor, :reader, :writer,
126
+ :lazy, :default, :nullable, :key, :serial, :column, :size, :length,
127
+ :index, :check
128
+ ]
129
+
130
+ # Adds property accessors for a field that you'd like to be able to modify. The DataMapper doesn't
131
+ # use the table schema to infer accessors, you must explicity call #property to add field accessors
132
+ # to your model.
133
+ #
134
+ # EXAMPLE:
135
+ # class CellProvider
136
+ # property :name, :string
137
+ # property :rating, :integer
138
+ # end
139
+ #
140
+ # att = CellProvider.new(:name => 'AT&T')
141
+ # att.rating = 3
142
+ # puts att.name, att.rating
143
+ #
144
+ # => AT&T
145
+ # => 3
146
+ #
147
+ # OPTIONS:
148
+ # * <tt>lazy</tt>: Lazy load the specified property (:lazy => true). False by default.
149
+ # * <tt>accessor</tt>: Set method visibility for the property accessors. Affects both
150
+ # reader and writer. Allowable values are :public, :protected, :private. Defaults to
151
+ # :public
152
+ # * <tt>reader</tt>: Like the accessor option but affects only the property reader.
153
+ # * <tt>writer</tt>: Like the accessor option but affects only the property writer.
154
+ # * <tt>protected</tt>: Alias for :reader => :public, :writer => :protected
155
+ # * <tt>private</tt>: Alias for :reader => :public, :writer => :private
156
+ def property(name, type, options = {})
157
+
158
+ options.each_pair do |k,v|
159
+ raise ArgumentError.new("#{k.inspect} is not a supported option in DataMapper::Base::PROPERTY_OPTIONS") unless PROPERTY_OPTIONS.include?(k)
160
+ end
161
+
162
+ visibility_options = [:public, :protected, :private]
163
+ reader_visibility = options[:reader] || options[:accessor] || :public
164
+ writer_visibility = options[:writer] || options[:accessor] || :public
165
+ writer_visibility = :protected if options[:protected]
166
+ writer_visibility = :private if options[:private]
167
+
168
+ raise(ArgumentError.new, "property visibility must be :public, :protected, or :private") unless visibility_options.include?(reader_visibility) && visibility_options.include?(writer_visibility)
169
+
170
+ mapping = database.schema[self].add_column(name.to_s.sub(/\?$/, '').to_sym, type, options)
171
+
172
+ property_getter(mapping, reader_visibility)
173
+ property_setter(mapping, writer_visibility)
174
+
175
+ if MAGIC_PROPERTIES.has_key?(name)
176
+ class_eval(&MAGIC_PROPERTIES[name])
177
+ end
178
+
179
+ return name
180
+ end
181
+
182
+ MAGIC_PROPERTIES = {
183
+ :updated_at => lambda { before_save { |x| x.updated_at = Time::now } },
184
+ :updated_on => lambda { before_save { |x| x.updated_on = Date::today } },
185
+ :created_at => lambda { before_create { |x| x.created_at = Time::now } },
186
+ :created_on => lambda { before_create { |x| x.created_on = Date::today } }
187
+ }
188
+
189
+ def property_getter(mapping, visibility = :public)
190
+ if mapping.lazy?
191
+ class_eval <<-EOS
192
+ #{visibility.to_s}
193
+ def #{mapping.name}
194
+ lazy_load!(#{mapping.name.inspect})
195
+ class << self;
196
+ attr_accessor #{mapping.name.inspect}
197
+ end
198
+ @#{mapping.name}
199
+ end
200
+ EOS
201
+ else
202
+ class_eval("#{visibility.to_s}; def #{mapping.name}; #{mapping.instance_variable_name} end") unless [ :public, :private, :protected ].include?(mapping.name)
203
+ end
204
+
205
+ if mapping.type == :boolean
206
+ class_eval("#{visibility.to_s}; def #{mapping.name.to_s.ensure_ends_with('?')}; #{mapping.instance_variable_name} end")
207
+ end
208
+
209
+ rescue SyntaxError
210
+ raise SyntaxError.new(mapping)
211
+ end
212
+
213
+ def property_setter(mapping, visibility = :public)
214
+ if mapping.lazy?
215
+ class_eval <<-EOS
216
+ #{visibility.to_s}
217
+ def #{mapping.name}=(value)
218
+ class << self;
219
+ attr_accessor #{mapping.name.inspect}
220
+ end
221
+ @#{mapping.name} = value
222
+ end
223
+ EOS
224
+ else
225
+ class_eval("#{visibility.to_s}; def #{mapping.name}=(value); #{mapping.instance_variable_name} = value end")
226
+ end
227
+ rescue SyntaxError
228
+ raise SyntaxError.new(mapping)
229
+ end
230
+
231
+ # Allows you to override the table name for a model.
232
+ # EXAMPLE:
233
+ # class WorkItem
234
+ # set_table_name 't_work_item_list'
235
+ # end
236
+ def set_table_name(value)
237
+ database.table(self).name = value
238
+ end
239
+ # An embedded value maps the values of an object to fields in the record of the object's owner.
240
+ # #embed takes a symbol to define the embedded class, options, and an optional block. See
241
+ # examples for use cases.
242
+ #
243
+ # EXAMPLE:
244
+ # class CellPhone < DataMapper::Base
245
+ # property :number, :string
246
+ #
247
+ # embed :owner, :prefix => true do
248
+ # property :name, :string
249
+ # property :address, :string
250
+ # end
251
+ # end
252
+ #
253
+ # my_phone = CellPhone.new
254
+ # my_phone.owner.name = "Nick"
255
+ # puts my_phone.owner.name
256
+ #
257
+ # => Nick
258
+ #
259
+ # OPTIONS:
260
+ # * <tt>prefix</tt>: define a column prefix, so instead of mapping :address to an 'address'
261
+ # column, it would map to 'owner_address' in the example above. If :prefix => true is
262
+ # specified, the prefix will be the name of the symbol given as the first parameter. If the
263
+ # prefix is a string the specified string will be used for the prefix.
264
+ # * <tt>lazy</tt>: lazy-load all embedded values at the same time. :lazy => true to enable.
265
+ # Disabled (false) by default.
266
+ # * <tt>accessor</tt>: Set method visibility for all embedded properties. Affects both
267
+ # reader and writer. Allowable values are :public, :protected, :private. Defaults to :public
268
+ # * <tt>reader</tt>: Like the accessor option but affects only embedded property readers.
269
+ # * <tt>writer</tt>: Like the accessor option but affects only embedded property writers.
270
+ # * <tt>protected</tt>: Alias for :reader => :public, :writer => :protected
271
+ # * <tt>private</tt>: Alias for :reader => :public, :writer => :private
272
+ #
273
+ def embed(name, options = {}, &block)
274
+ EmbeddedValue::define(self, name, options, &block)
275
+ end
276
+
277
+ def properties
278
+ @properties
279
+ end
280
+
281
+ # Creates a composite index for an arbitrary number of database columns. Note that
282
+ # it also is possible to specify single indexes directly for each property.
283
+ #
284
+ # === EXAMPLE WITH COMPOSITE INDEX:
285
+ # class Person < DataMapper::Base
286
+ # property :server_id, :integer
287
+ # property :name, :string
288
+ #
289
+ # index [:server_id, :name]
290
+ # end
291
+ #
292
+ # === EXAMPLE WITH COMPOSITE UNIQUE INDEX:
293
+ # class Person < DataMapper::Base
294
+ # property :server_id, :integer
295
+ # property :name, :string
296
+ #
297
+ # index [:server_id, :name], :unique => true
298
+ # end
299
+ #
300
+ # === SINGLE INDEX EXAMPLES:
301
+ # * property :name, :index => true
302
+ # * property :name, :index => :unique
303
+ def index(indexes, unique = false)
304
+ if indexes.kind_of?(Array) # if given an index of multiple columns
305
+ database.schema[self].add_composite_index(indexes, unique)
306
+ else
307
+ raise ArgumentError.new("You must supply an array for the composite index")
308
+ end
309
+ end
310
+
311
+ end
312
+
313
+ # Lazy-loads the attributes for a loaded_set, then overwrites the accessors
314
+ # for the named methods so that the lazy_loading is skipped the second time.
315
+ def lazy_load!(*names)
316
+
317
+ names = names.map { |name| name.to_sym }.reject { |name| lazy_loaded_attributes.include?(name) }
318
+
319
+ reset_attribute = lambda do |instance|
320
+ singleton_class = (class << instance; self end)
321
+ names.each do |name|
322
+ instance.lazy_loaded_attributes << name
323
+ singleton_class.send(:attr_accessor, name)
324
+ end
325
+ end
326
+
327
+ unless names.empty? || new_record? || loaded_set.nil?
328
+
329
+ key = database_context.table(self.class).key.to_sym
330
+ keys_to_select = loaded_set.map do |instance|
331
+ instance.send(key)
332
+ end
333
+
334
+ database_context.all(
335
+ self.class,
336
+ :select => ([key] + names),
337
+ :reload => true,
338
+ key => keys_to_select
339
+ ).each(&reset_attribute)
340
+ else
341
+ reset_attribute[self]
342
+ end
343
+ end
344
+
345
+ # Mass-assign mapped fields.
346
+ def attributes=(values_hash)
347
+ table = database_context.table(self.class)
348
+
349
+ values_hash.delete_if do |key, value|
350
+ !self.class.public_method_defined?("#{key}=")
351
+ end.each_pair do |key, value|
352
+ if respond_to?(key)
353
+ send("#{key}=", value)
354
+ elsif column = table[key]
355
+ instance_variable_set(column.instance_variable_name, value)
356
+ end
357
+ end
358
+ end
359
+
360
+ def database_context
361
+ @database_context || ( @database_context = database )
362
+ end
363
+
364
+ def database_context=(value)
365
+ @database_context = value
366
+ end
367
+
368
+ def logger
369
+ self.class.logger
370
+ end
371
+
372
+ def new_record?
373
+ @new_record.nil? || @new_record
374
+ end
375
+
376
+ def ==(other)
377
+ other.is_a?(self.class) && private_attributes == other.send(:private_attributes)
378
+ end
379
+
380
+ # Returns the difference between two objects, in terms of their attributes.
381
+ def ^(other)
382
+ results = {}
383
+
384
+ self_attributes, other_attributes = attributes, other.attributes
385
+
386
+ self_attributes.each_pair do |k,v|
387
+ other_value = other_attributes[k]
388
+ unless v == other_value
389
+ results[k] = [v, other_value]
390
+ end
391
+ end
392
+
393
+ results
394
+ end
395
+
396
+ def lazy_loaded_attributes
397
+ @lazy_loaded_attributes || @lazy_loaded_attributes = Set.new
398
+ end
399
+
400
+ def loaded_attributes
401
+ pairs = {}
402
+
403
+ database_context.table(self).columns.each do |column|
404
+ pairs[column.name] = instance_variable_get(column.instance_variable_name)
405
+ end
406
+
407
+ pairs
408
+ end
409
+
410
+ def update_attributes(update_hash)
411
+ self.attributes = update_hash
412
+ self.save
413
+ end
414
+
415
+ def attributes
416
+ pairs = {}
417
+
418
+ database_context.table(self).columns.each do |column|
419
+ if self.class.public_method_defined?(column.name)
420
+ lazy_load!(column.name) if column.lazy?
421
+ value = instance_variable_get(column.instance_variable_name)
422
+ pairs[column.name] = column.type == :class ? value.to_s : value
423
+ end
424
+ end
425
+
426
+ pairs
427
+ end
428
+
429
+ def unsafe_attributes=(values_hash)
430
+ table = database_context.table(self.class)
431
+
432
+ values_hash.each_pair do |key, value|
433
+ if respond_to?(key)
434
+ send("#{key}=", value)
435
+ elsif column = table[key]
436
+ instance_variable_set(column.instance_variable_name, value)
437
+ end
438
+ end
439
+ end
440
+
441
+ def dirty?
442
+ result = database_context.table(self).columns.any? do |column|
443
+ if column.type == :object
444
+ Marshal.dump(self.instance_variable_get(column.instance_variable_name)) != original_values[column.name]
445
+ else
446
+ self.instance_variable_get(column.instance_variable_name) != original_values[column.name]
447
+ end
448
+ end
449
+
450
+ return true if result
451
+
452
+ loaded_associations.any? do |loaded_association|
453
+ loaded_association.dirty?
454
+ end
455
+ end
456
+
457
+ def dirty_attributes
458
+ pairs = {}
459
+
460
+ database_context.table(self).columns.each do |column|
461
+ value = instance_variable_get(column.instance_variable_name)
462
+ if value != original_values[column.name] && (!new_record? || !column.serial?)
463
+ pairs[column.name] = column.type != :object ? value : YAML.dump(value)
464
+ end
465
+ end
466
+
467
+ pairs
468
+ end
469
+
470
+ def original_values=(values)
471
+ values.each_pair do |k,v|
472
+ original_values[k] = case v
473
+ when String, Date, Time then v.dup
474
+ # when column.type == :object then Marshal.dump(v)
475
+ else v
476
+ end
477
+ end
478
+ end
479
+
480
+ def original_values
481
+ class << self
482
+ attr_reader :original_values
483
+ end
484
+
485
+ @original_values = {}
486
+ end
487
+
488
+ def loaded_set=(value)
489
+ value << self
490
+ @loaded_set = value
491
+ end
492
+
493
+ def inspect
494
+ inspected_attributes = attributes.map { |k,v| "@#{k}=#{v.inspect}" }
495
+
496
+ instance_variables.each do |name|
497
+ if instance_variable_get(name).kind_of?(Associations::HasManyAssociation)
498
+ inspected_attributes << "#{name}=#{instance_variable_get(name).inspect}"
499
+ end
500
+ end
501
+
502
+ "#<%s:0x%x @new_record=%s, %s>" % [self.class.name, (object_id * 2), new_record?, inspected_attributes.join(', ')]
503
+ end
504
+
505
+ def loaded_associations
506
+ @loaded_associations || @loaded_associations = []
507
+ end
508
+
509
+ def key=(value)
510
+ key_column = database_context.table(self.class).key
511
+ @__key = key_column.type_cast_value(value)
512
+ instance_variable_set(key_column.instance_variable_name, @__key)
513
+ end
514
+
515
+ def key
516
+ @__key || @__key = begin
517
+ key_column = database_context.table(self.class).key
518
+ key_column.type_cast_value(instance_variable_get(key_column.instance_variable_name))
519
+ end
520
+ end
521
+
522
+ private
523
+
524
+ # return all attributes, regardless of their visibility
525
+ def private_attributes
526
+ pairs = {}
527
+
528
+ database_context.table(self).columns.each do |column|
529
+ lazy_load!(column.name) if column.lazy?
530
+ value = instance_variable_get(column.instance_variable_name)
531
+ pairs[column.name] = column.type == :class ? value.to_s : value
532
+ end
533
+
534
+ pairs
535
+ end
536
+
537
+ end
538
+ end