mongodb-mongo_record 0.0.2

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.
@@ -0,0 +1,830 @@
1
+ #--
2
+ # Copyright (C) 2009 10gen Inc.
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify it
5
+ # under the terms of the GNU Affero General Public License, version 3, as
6
+ # published by the Free Software Foundation.
7
+ #
8
+ # This program is distributed in the hope that it will be useful, but WITHOUT
9
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
10
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
11
+ # for more details.
12
+ #
13
+ # You should have received a copy of the GNU Affero General Public License
14
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
15
+ #++
16
+
17
+ require 'rubygems'
18
+ require 'mongo/types/objectid'
19
+ require 'mongo/cursor'
20
+ require 'mongo_record/convert'
21
+ require 'mongo_record/sql'
22
+
23
+ class String
24
+ # Convert this String to an ObjectID.
25
+ def to_oid
26
+ XGen::Mongo::Driver::ObjectID.from_string(self)
27
+ end
28
+ end
29
+
30
+ class XGen::Mongo::Driver::ObjectID
31
+ # Convert this object to an ObjectID.
32
+ def to_oid
33
+ self
34
+ end
35
+ end
36
+
37
+ module MongoRecord
38
+
39
+ class PKFactory
40
+ def create_pk(row)
41
+ return row if row[:_id]
42
+ row.delete(:_id) # in case it is nil
43
+ row['_id'] ||= XGen::Mongo::Driver::ObjectID.new
44
+ row
45
+ end
46
+ end
47
+
48
+ class MongoError < StandardError #:nodoc:
49
+ end
50
+ class PreparedStatementInvalid < MongoError #:nodoc:
51
+ end
52
+ class RecordNotFound < MongoError #:nodoc:
53
+ end
54
+ class RecordNotSaved < MongoError #:nodoc:
55
+ end
56
+
57
+ # A superclass for database collection instances. The API is very similar
58
+ # to ActiveRecord. See #find for examples.
59
+ #
60
+ # If you override initialize, make sure to call the superclass version,
61
+ # passing it the database row or hash that it was given.
62
+ #
63
+ # Example:
64
+ #
65
+ # class MP3Track < MongoRecord::Base
66
+ # collection_name :mp3_track
67
+ # fields :artist, :album, :song, :track
68
+ # def to_s
69
+ # "artist: #{self.artist}, album: #{self.album}, song: #{self.song}, track: #{track}"
70
+ # end
71
+ # end
72
+ #
73
+ # track = MP3Track.find_by_song('She Blinded Me With Science')
74
+ # puts track.to_s
75
+ #
76
+ # The database connection defaults to the global $db. You can set the
77
+ # connection using MongoRecord::Base.connection= and read it with
78
+ # MongoRecord::Base.connection.
79
+ #
80
+ # # Set the connection to something besides $db
81
+ # MongoRecord::Base.connection = connect('my-database')
82
+ class Base
83
+
84
+ @@connection = nil
85
+
86
+ class << self # Class methods
87
+
88
+ # Return the database connection. The default value is # <code>$db</code>.
89
+ def connection
90
+ conn = @@connection || $db
91
+ raise "connection not defined" unless conn
92
+ conn
93
+ end
94
+
95
+ # Set the database connection. If the connection is set to +nil+, then
96
+ # <code>$db</code> will be used.
97
+ def connection=(val)
98
+ @@connection = val
99
+ @@connection.pk_factory = PKFactory.new unless @@connection.pk_factory
100
+ end
101
+
102
+ # This method only exists so that MongoRecord::Base and
103
+ # ActiveRecord::Base can live side by side.
104
+ def instantiate(row={})
105
+ new(row)
106
+ end
107
+
108
+ # Get ready to save information about +subclass+.
109
+ def inherited(subclass)
110
+ subclass.instance_variable_set("@coll_name", class_name_to_field_name(subclass.name)) # default name
111
+ subclass.instance_variable_set("@field_names", []) # array of scalars names (symbols)
112
+ subclass.instance_variable_set("@subobjects", {}) # key = name (symbol), value = class
113
+ subclass.instance_variable_set("@arrays", {}) # key = name (symbol), value = class
114
+ end
115
+
116
+ # Call this method to set the Mongo collection name for this class.
117
+ # The default value is the class name turned into
118
+ # lower_case_with_underscores.
119
+ def collection_name(coll_name)
120
+ @coll_name = coll_name
121
+ field(:_id, :_ns, :_update)
122
+ end
123
+
124
+ # Creates one or more collection fields. Each field will be saved to
125
+ # and loaded from the database. The fields named "_id" and "_ns" are
126
+ # automatically saved and loaded.
127
+ #
128
+ # The method "field" is also called "fields"; you can use either one.
129
+ def field(*fields)
130
+ fields.each { |field|
131
+ field = field.to_sym
132
+ unless @field_names.include?(field)
133
+ ivar_name = "@" + field.to_s
134
+ define_method(field, lambda { instance_variable_get(ivar_name) })
135
+ define_method("#{field}=".to_sym, lambda { |val| instance_variable_set(ivar_name, val) })
136
+ define_method("#{field}?".to_sym, lambda {
137
+ val = instance_variable_get(ivar_name)
138
+ val != nil && (!val.kind_of?(String) || val != '')
139
+ })
140
+ @field_names << field
141
+ end
142
+ }
143
+ end
144
+ alias_method :fields, :field
145
+
146
+ # Return the field names.
147
+ def field_names; @field_names; end
148
+
149
+ # Return the names of all instance variables that hold objects
150
+ # declared using has_one. The names do not start with '@'.
151
+ #
152
+ # These are not necessarily MongoRecord::Subobject subclasses.
153
+ def subobjects; @subobjects; end
154
+
155
+ # Return the names of all instance variables that hold objects
156
+ # declared using has_many. The names do not start with '@'.
157
+ def arrays; @arrays; end
158
+
159
+ # Return the names of all fields, subobjects, and arrays.
160
+ def mongo_ivar_names; @field_names + @subobjects.keys + @arrays.keys; end
161
+
162
+ # Tell Mongo about a subobject (which need not be a
163
+ # MongoRecord::Subobject).
164
+ #
165
+ # Options:
166
+ # <code>:class_name<code> - Name of the class of the subobject.
167
+ def has_one(name, options={})
168
+ name = name.to_sym
169
+ unless @subobjects[name]
170
+ ivar_name = "@" + name.to_s
171
+ define_method(name, lambda { instance_variable_get(ivar_name) })
172
+ define_method("#{name}=".to_sym, lambda { |val| instance_variable_set(ivar_name, val) })
173
+ define_method("#{name}?".to_sym, lambda {
174
+ val = instance_variable_get(ivar_name)
175
+ val != nil && (!val.kind_of?(String) || val != '')
176
+ })
177
+ klass_name = options[:class_name] || field_name_to_class_name(name)
178
+ @subobjects[name] = Kernel.const_get(klass_name)
179
+ end
180
+ end
181
+
182
+ # Tells Mongo about an array of subobjects (which need not be
183
+ # MongoRecord::Subobjects).
184
+ #
185
+ # Options:
186
+ # <code>:class_name</code> - Name of the class of the subobject.
187
+ def has_many(name, options={})
188
+ name = name.to_sym
189
+ unless @arrays[name]
190
+ ivar_name = "@" + name.to_s
191
+ define_method(name, lambda { instance_variable_get(ivar_name) })
192
+ define_method("#{name}=".to_sym, lambda { |val| instance_variable_set(ivar_name, val) })
193
+ define_method("#{name}?".to_sym, lambda { !instance_variable_get(ivar_name).empty? })
194
+ klass_name = options[:class_name] || field_name_to_class_name(name)
195
+ @arrays[name] = Kernel.const_get(klass_name)
196
+ end
197
+ end
198
+
199
+ # Tells Mongo that this object has and many belongs to another object.
200
+ # A no-op.
201
+ def has_and_belongs_to_many(name, options={})
202
+ end
203
+
204
+ # Tells Mongo that this object belongs to another. A no-op.
205
+ def belongs_to(name, options={})
206
+ end
207
+
208
+ # The collection object for this class, which will be different for
209
+ # every subclass of MongoRecord::Base.
210
+ def collection
211
+ connection.collection(@coll_name.to_s)
212
+ end
213
+
214
+ # Find one or more database objects.
215
+ #
216
+ # * Find by id (a single id or an array of ids) returns one record or a Cursor.
217
+ #
218
+ # * Find :first returns the first record that matches the options used
219
+ # or nil if not found.
220
+ #
221
+ # * Find :all records; returns a Cursor that can iterate over raw
222
+ # records.
223
+ #
224
+ # Options:
225
+ #
226
+ # <code>:conditions</code> - Hash where key is field name and value is
227
+ # field value. Value may be a simple value like a string, number, or
228
+ # regular expression.
229
+ #
230
+ # <code>:select</code> - Single field name or list of field names. If
231
+ # not specified, all fields are returned. Names may be symbols or
232
+ # strings. The database always returns _id and _ns fields.
233
+ #
234
+ # <code>:order</code> - If a symbol, orders by that field in ascending
235
+ # order. If a string like "field1 asc, field2 desc, field3", then
236
+ # sorts those fields in the specified order (default is ascending). If
237
+ # an array, each element is either a field name or symbol (which will
238
+ # be sorted in ascending order) or a hash where key =isfield and value
239
+ # is 'asc' or 'desc' (case-insensitive), 1 or -1, or if any other value
240
+ # then true == 1 and false/nil == -1.
241
+ #
242
+ # <code>:limit</code> - Maximum number of records to return.
243
+ #
244
+ # <code>:offset</code> - Number of records to skip.
245
+ #
246
+ # <code>:where</code> - A string containing a JavaScript expression.
247
+ # This expression is run by the database server against each record
248
+ # found after the :conditions are run.
249
+ #
250
+ # Examples for find by id:
251
+ # Person.find("48e5307114f4abdf00dfeb86") # returns the object for this ID
252
+ # Person.find(["a_hex_id", "another_hex_id"]) # returns a Cursor over these two objects
253
+ # Person.find(["a_hex_id"]) # returns a Cursor over the object with this ID
254
+ # Person.find("a_hex_id", :conditions => "admin = 1", :order => "created_on DESC")
255
+ #
256
+ # Examples for find first:
257
+ # Person.find(:first) # returns the first object in the collection
258
+ # Person.find(:first, :conditions => ["user_name = ?", user_name])
259
+ # Person.find(:first, :order => "created_on DESC", :offset => 5)
260
+ # Person.find(:first, :order => {:created_on => -1}, :offset => 5) # same as previous example
261
+ #
262
+ # Examples for find all:
263
+ # Person.find(:all) # returns a Cursor over all objects in the collection
264
+ # Person.find(:all, :conditions => ["category = ?, category], :limit => 50)
265
+ # Person.find(:all, :offset => 10, :limit => 10)
266
+ # Person.find(:all, :select => :name) # Only returns name (and _id) fields
267
+ #
268
+ # Find_by_*
269
+ # Person.find_by_name_and_age("Spongebob", 42)
270
+ # Person.find_all_by_name("Fred")
271
+ #
272
+ # Mongo-specific example:
273
+ # Person.find(:all, :where => "this.address.city == 'New York' || this.age = 42")
274
+ #
275
+ # As a side note, the :order, :limit, and :offset options are passed
276
+ # on to the Cursor (after the :order option is rewritten to be a
277
+ # hash). So
278
+ # Person.find(:all, :offset => 10, :limit => 10, :order => :created_on)
279
+ # is the same as
280
+ # Person.find(:all).skip(10).limit(10).sort({:created_on => 1})
281
+ def find(*args)
282
+ options = extract_options_from_args!(args)
283
+ case args.first
284
+ when :first
285
+ find_initial(options)
286
+ when :all
287
+ find_every(options)
288
+ else
289
+ find_from_ids(args, options)
290
+ end
291
+ end
292
+
293
+ # Returns all records matching mql. Not yet implemented.
294
+ def find_by_mql(mql) # :nodoc:
295
+ raise "not implemented"
296
+ end
297
+ alias_method :find_by_sql, :find_by_mql
298
+
299
+ # Returns the number of matching records.
300
+ def count(options={})
301
+ criteria = criteria_from(options[:conditions]).merge!(where_func(options[:where]))
302
+ collection.count(criteria)
303
+ end
304
+
305
+ # Deletes the record with the given id from the collection.
306
+ def delete(id)
307
+ collection.remove({:_id => id})
308
+ end
309
+ alias_method :remove, :delete
310
+
311
+ # Load the object with +id+ and delete it.
312
+ def destroy(id)
313
+ id.is_a?(Array) ? id.each { |oid| destroy(oid) } : find(id).destroy
314
+ end
315
+
316
+ # Not yet implemented.
317
+ def update_all(updates, conditions = nil)
318
+ # TODO
319
+ raise "not yet implemented"
320
+ end
321
+
322
+ # Destroy all objects that match +conditions+. Warning: if
323
+ # +conditions+ is +nil+, all records in the collection will be
324
+ # destroyed.
325
+ def destroy_all(conditions = nil)
326
+ find(:all, :conditions => conditions).each { |object| object.destroy }
327
+ end
328
+
329
+ # Deletes all records that match +condition+, which can be a
330
+ # Mongo-style hash or an ActiveRecord-like hash. Examples:
331
+ # Person.destroy_all "name like '%fred%' # SQL WHERE clause
332
+ # Person.destroy_all ["name = ?", 'Fred'] # Rails condition
333
+ # Person.destroy_all {:name => 'Fred'} # Mongo hash
334
+ def delete_all(conditions=nil)
335
+ collection.remove(criteria_from(conditions))
336
+ end
337
+
338
+ # Creates, saves, and returns a new database object.
339
+ def create(values_hash)
340
+ object = self.new(values_hash)
341
+ object.save
342
+ object
343
+ end
344
+
345
+ # Finds the record from the passed +id+, instantly saves it with the passed +attributes+ (if the validation permits it),
346
+ # and returns it. If the save fails under validations, the unsaved object is still returned.
347
+ #
348
+ # The arguments may also be given as arrays in which case the update method is called for each pair of +id+ and
349
+ # +attributes+ and an array of objects is returned.
350
+ # =>
351
+ # Example of updating one record:
352
+ # Person.update(15, {:user_name => 'Samuel', :group => 'expert'})
353
+ #
354
+ # Example of updating multiple records:
355
+ # people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy"} }
356
+ # Person.update(people.keys, people.values)
357
+ def update(id, attributes)
358
+ if id.is_a?(Array)
359
+ i = -1
360
+ id.collect { |id| i += 1; update(id, attributes[i]) }
361
+ else
362
+ object = find(id)
363
+ object.update_attributes(attributes)
364
+ object
365
+ end
366
+ end
367
+
368
+ # Handles find_* methods such as find_by_name, find_all_by_shoe_size,
369
+ # and find_or_create_by_name.
370
+ def method_missing(sym, *args)
371
+ if match = /^find_(all_by|by)_([_a-zA-Z]\w*)$/.match(sym.to_s)
372
+ find_how_many = ($1 == 'all_by') ? :all : :first
373
+ field_names = $2.split(/_and_/)
374
+ super unless all_fields_exist?(field_names)
375
+ search = search_from_names_and_values(field_names, args)
376
+ self.find(find_how_many, {:conditions => search}, *args[field_names.length..-1])
377
+ elsif match = /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/.match(sym.to_s)
378
+ create = $1 == 'create'
379
+ field_names = $2.split(/_and_/)
380
+ super unless all_fields_exist?(field_names)
381
+ search = search_from_names_and_values(field_names, args)
382
+ row = self.find(:first, {:conditions => search})
383
+ return self.new(row) if row # found
384
+ obj = self.new(search.merge(args[field_names.length] || {})) # new object using search and remainder of args
385
+ obj.save if create
386
+ obj
387
+ else
388
+ super
389
+ end
390
+ end
391
+
392
+ private
393
+
394
+ def extract_options_from_args!(args)
395
+ args.last.is_a?(Hash) ? args.pop : {}
396
+ end
397
+
398
+ def find_initial(options)
399
+ criteria = criteria_from(options[:conditions]).merge!(where_func(options[:where]))
400
+ fields = fields_from(options[:select])
401
+ row = collection.find(criteria, :fields => fields, :limit => 1).next_object
402
+ (row.nil? || row['_id'] == nil) ? nil : self.new(row)
403
+ end
404
+
405
+ def find_every(options)
406
+ criteria = criteria_from(options[:conditions]).merge!(where_func(options[:where]))
407
+
408
+ find_options = {}
409
+ find_options[:fields] = fields_from(options[:select]) if options[:select]
410
+ find_options[:limit] = options[:limit].to_i if options[:limit]
411
+ find_options[:offset] = options[:offset].to_i if options[:offset]
412
+ find_options[:sort] = sort_by_from(options[:order]) if options[:order]
413
+
414
+ cursor = collection.find(criteria, find_options)
415
+
416
+ # Override cursor.next_object so it returns a new instance of this class
417
+ eval "def cursor.next_object; #{self.name}.new(super()); end"
418
+ cursor
419
+ end
420
+
421
+ def find_from_ids(ids, options)
422
+ ids = ids.to_a.flatten.compact.uniq
423
+ raise RecordNotFound, "Couldn't find #{name} without an ID" unless ids.length > 0
424
+
425
+ criteria = criteria_from(options[:conditions]).merge!(where_func(options[:where]))
426
+ criteria[:_id] = ids_clause(ids)
427
+ fields = fields_from(options[:select])
428
+
429
+ if ids.length == 1
430
+ row = collection.find(criteria, :fields => fields, :limit => 1).next_object
431
+ raise RecordNotFound, "Couldn't find #{name} with ID=#{ids[0]} #{criteria.inspect}" if row == nil || row.empty?
432
+ self.new(row)
433
+ else
434
+ find_options = {}
435
+ find_options[:fields] = fields if fields
436
+ find_options[:sort] = sort_by_from(options[:order]) if options[:order]
437
+
438
+ cursor = collection.find(criteria, find_options)
439
+
440
+ # Override cursor.next_object so it returns a new instance of this class
441
+ eval "def cursor.next_object; #{self.name}.new(super()); end"
442
+ cursor
443
+ end
444
+ end
445
+
446
+ def ids_clause(ids)
447
+ ids.length == 1 ? ids[0].to_oid : {'$in' => ids.collect{|id| id.to_oid}}
448
+ end
449
+
450
+ # Returns true if all field_names are in @field_names.
451
+ def all_fields_exist?(field_names)
452
+ field_names.collect! {|f| f == 'id' ? '_id' : f}
453
+ (field_names - @field_names.collect{|f| f.to_s}).empty?
454
+ end
455
+
456
+ # Returns a db search hash, given field_names and values.
457
+ def search_from_names_and_values(field_names, values)
458
+ h = {}
459
+ field_names.each_with_index { |iv, i| h[iv.to_s] = values[i] }
460
+ h
461
+ end
462
+
463
+ # Given a "SymbolOrStringLikeThis", return the string "symbol_or_string_like_this".
464
+ def class_name_to_field_name(name)
465
+ name.gsub(/([A-Z])/, '_\1').downcase.sub(/^_/, '')
466
+ end
467
+
468
+ # Given a "symbol_or_string_like_this", return the string "SymbolOrStringLikeThis".
469
+ def field_name_to_class_name(name)
470
+ name = name.to_s.dup.gsub(/_([a-z])/) {$1.upcase}
471
+ name[0,1] = name[0,1].upcase
472
+ name
473
+ end
474
+
475
+ protected
476
+
477
+ # Turns array, string, or hash conditions into something useable by Mongo.
478
+ # ["name='%s' and group_id='%s'", "foo'bar", 4] returns {:name => 'foo''bar', :group_id => 4}
479
+ # "name='foo''bar' and group_id='4'" returns {:name => 'foo''bar', :group_id => 4}
480
+ # { :name => "foo'bar", :group_id => 4 } returns the hash, modified for Mongo
481
+ def criteria_from(condition) # :nodoc:
482
+ case condition
483
+ when Array
484
+ criteria_from_array(condition)
485
+ when String
486
+ criteria_from_string(condition)
487
+ when Hash
488
+ criteria_from_hash(condition)
489
+ else
490
+ {}
491
+ end
492
+ end
493
+
494
+ # Substitutes values at the end of an array into the string at its
495
+ # start, sanitizing strings in the values. Then passes the string on
496
+ # to criteria_from_string.
497
+ def criteria_from_array(condition) # :nodoc:
498
+ str, *values = condition
499
+ sql = if values.first.kind_of?(Hash) and str =~ /:\w+/
500
+ replace_named_bind_variables(str, values.first)
501
+ elsif str.include?('?')
502
+ replace_bind_variables(str, values)
503
+ else
504
+ str % values.collect {|value| quote(value) }
505
+ end
506
+ criteria_from_string(sql)
507
+ end
508
+
509
+ # Turns a string into a Mongo search condition hash.
510
+ def criteria_from_string(sql) # :nodoc:
511
+ MongoRecord::SQL::Parser.parse_where(sql)
512
+ end
513
+
514
+ # Turns a hash that ActiveRecord would expect into one for Mongo.
515
+ def criteria_from_hash(condition) # :nodoc:
516
+ h = {}
517
+ condition.each { |k,v|
518
+ h[k] = case v
519
+ when Array
520
+ {'$in' => k == 'id' || k == '_id' ? v.collect{ |val| val.to_oid} : v} # if id, can't pass in string; must be ObjectID
521
+ when Range
522
+ {'$gte' => v.first, '$lte' => v.last}
523
+ else
524
+ k == 'id' || k == '_id' ? v.to_oid : v
525
+ end
526
+ }
527
+ h
528
+ end
529
+
530
+ # Returns a hash useable by Mongo for applying +func+ on the db server.
531
+ # +func+ must be +nil+ or a JavaScript expression or function in a
532
+ # string.
533
+ def where_func(func) # :nodoc:
534
+ func ? {'$where' => func} : {}
535
+ end
536
+
537
+ def replace_named_bind_variables(str, h) # :nodoc:
538
+ str.gsub(/:(\w+)/) do
539
+ match = $1.to_sym
540
+ if h.include?(match)
541
+ quoted_bind_var(h[match])
542
+ else
543
+ raise PreparedStatementInvalid, "missing value for :#{match} in #{str}" # TODO this gets swallowed in find()
544
+ end
545
+ end
546
+ end
547
+
548
+ def replace_bind_variables(str, values) # :nodoc:
549
+ raise "parameter count does not match value count" unless str.count('?') == values.length
550
+ bound = values.dup
551
+ str.gsub('?') { quoted_bind_var(bound.shift) }
552
+ end
553
+
554
+ def quoted_bind_var(val) # :nodoc:
555
+ case val
556
+ when Array
557
+ val.collect{|v| quote(v)}.join(',')
558
+ else
559
+ quote(val)
560
+ end
561
+ end
562
+
563
+ # Returns value quoted if appropriate (if it's a string).
564
+ def quote(val) # :nodoc:
565
+ return val unless val.is_a?(String)
566
+ return "'#{val.gsub(/\'/, "\\\\'")}'" # " <= for Emacs font-lock
567
+ end
568
+
569
+ def fields_from(a) # :nodoc:
570
+ return nil unless a
571
+ a = [a] unless a.kind_of?(Array)
572
+ a += ['_id'] # always return _id
573
+ a.uniq.collect { |k| k.to_s }
574
+ end
575
+
576
+ def sort_by_from(option) # :nodoc:
577
+ return nil unless option
578
+ sort_by = []
579
+ case option
580
+ when Symbol # Single value
581
+ sort_by << {option.to_s => 1}
582
+ when String
583
+ # TODO order these by building an array of hashes
584
+ fields = option.split(',')
585
+ fields.each {|f|
586
+ name, order = f.split
587
+ order ||= 'asc'
588
+ sort_by << {name.to_s => sort_value_from_arg(order)}
589
+ }
590
+ when Array # Array of field names; assume ascending sort
591
+ # TODO order these by building an array of hashes
592
+ sort_by = option.collect {|o| {o.to_s => 1}}
593
+ else # Hash (order of sorts is not guaranteed)
594
+ sort_by = option.collect {|k, v| {k.to_s => sort_value_from_arg(v)}}
595
+ end
596
+ return nil unless sort_by.length > 0
597
+ sort_by
598
+ end
599
+
600
+ # Turns "asc" into 1, "desc" into -1, and other values into 1 or -1.
601
+ def sort_value_from_arg(arg) # :nodoc:
602
+ case arg
603
+ when /^asc/i
604
+ arg = 1
605
+ when /^desc/i
606
+ arg = -1
607
+ when Number
608
+ arg.to_i >= 0 ? 1 : -1
609
+ else
610
+ arg ? 1 : -1
611
+ end
612
+ end
613
+
614
+ # Overwrite the default class equality method to provide support for association proxies.
615
+ def ===(object)
616
+ object.is_a?(self)
617
+ end
618
+
619
+ end # End of class methods
620
+
621
+ public
622
+
623
+ # Initialize a new object with either a hash of values or a row returned
624
+ # from the database.
625
+ def initialize(row={})
626
+ case row
627
+ when Hash
628
+ row.each { |k, val|
629
+ k = '_id' if k == 'id' # Rails helper
630
+ init_ivar("@#{k}", val)
631
+ }
632
+ else
633
+ row.instance_variables.each { |iv|
634
+ init_ivar(iv, row.instance_variable_get(iv))
635
+ }
636
+ end
637
+ # Default values for remaining fields
638
+ (self.class.field_names + self.class.subobjects.keys).each { |iv|
639
+ iv = "@#{iv}"
640
+ instance_variable_set(iv, nil) unless instance_variable_defined?(iv)
641
+ }
642
+ self.class.arrays.keys.each { |iv|
643
+ iv = "@#{iv}"
644
+ instance_variable_set(iv, []) unless instance_variable_defined?(iv)
645
+ }
646
+ yield self if block_given?
647
+ end
648
+
649
+ # Set the id of this object. Normally not called by user code.
650
+ def id=(val); @_id = (val == '' ? nil : val); end
651
+
652
+ # Return this object's id.
653
+ def id; @_id ? @_id.to_s : nil; end
654
+
655
+ # Return true if the +comparison_object+ is the same object, or is of
656
+ # the same type and has the same id.
657
+ def ==(comparison_object)
658
+ comparison_object.equal?(self) ||
659
+ (comparison_object.instance_of?(self.class) &&
660
+ comparison_object.id == id &&
661
+ !comparison_object.new_record?)
662
+ end
663
+
664
+ # Delegate to ==
665
+ def eql?(comparison_object)
666
+ self == (comparison_object)
667
+ end
668
+
669
+ # Delegate to id in order to allow two records of the same type and id to work with something like:
670
+ # [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ]
671
+ def hash
672
+ id.hash
673
+ end
674
+
675
+ # Rails convenience method. Return this object's id as a string.
676
+ def to_param
677
+ @_id.to_s
678
+ end
679
+
680
+ # Save self and returns true if the save was successful, false if not.
681
+ def save
682
+ create_or_update
683
+ end
684
+
685
+ # Save self and returns true if the save was successful and raises
686
+ # RecordNotSaved if not.
687
+ def save!
688
+ create_or_update || raise(RecordNotSaved)
689
+ end
690
+
691
+ # Return true if this object is new---that is, does not yet have an id.
692
+ def new_record?
693
+ @_id == nil
694
+ end
695
+
696
+ # Convert this object to a Mongo value suitable for saving to the
697
+ # database.
698
+ def to_mongo_value
699
+ h = {}
700
+ self.class.mongo_ivar_names.each {|iv| h[iv] = instance_variable_get("@#{iv}").to_mongo_value }
701
+ h
702
+ end
703
+
704
+ # Save self to the database and set the id.
705
+ def create
706
+ set_create_times
707
+ with_id = self.class.collection.insert(to_mongo_value)
708
+ @_id = with_id['_id'] || with_id[:_id]
709
+ self
710
+ end
711
+
712
+ # Save self to the database. Return +false+ if there was an error,
713
+ # +self+ if all is well.
714
+ def update
715
+ set_update_times
716
+ row = self.class.collection.insert(to_mongo_value)
717
+ if row['_id'].to_s != @_id.to_s
718
+ return false
719
+ end
720
+ self
721
+ end
722
+
723
+ # Remove self from the database and set @_id to nil. If self has no
724
+ # @_id, does nothing.
725
+ def delete
726
+ if @_id
727
+ self.class.collection.remove({:_id => self._id})
728
+ @_id = nil
729
+ end
730
+ end
731
+ alias_method :remove, :delete
732
+
733
+ # Delete and freeze self.
734
+ def destroy
735
+ delete
736
+ freeze
737
+ end
738
+
739
+ #--
740
+ # ================================================================
741
+ # These methods exist so we can plug in ActiveRecord validation, etc.
742
+ # ================================================================
743
+ #++
744
+
745
+ # Updates a single attribute and saves the record. This is especially
746
+ # useful for boolean flags on existing records. Note: This method is
747
+ # overwritten by the Validation module that'll make sure that updates
748
+ # made with this method doesn't get subjected to validation checks.
749
+ # Hence, attributes can be updated even if the full object isn't valid.
750
+ def update_attribute(name, value)
751
+ send(name.to_s + '=', value)
752
+ save
753
+ end
754
+
755
+ # Updates all the attributes from the passed-in Hash and saves the
756
+ # record. If the object is invalid, the saving will fail and false will
757
+ # be returned.
758
+ def update_attributes(attributes)
759
+ self.attributes = attributes
760
+ save
761
+ end
762
+
763
+ # Updates an object just like Base.update_attributes but calls save!
764
+ # instead of save so an exception is raised if the record is invalid.
765
+ def update_attributes!(attributes)
766
+ self.attributes = attributes
767
+ save!
768
+ end
769
+
770
+ # Does nothing.
771
+ def attributes_from_column_definition; end
772
+
773
+ # ================================================================
774
+
775
+ private
776
+
777
+ def create_or_update
778
+ result = new_record? ? create : update
779
+ result != false
780
+ end
781
+
782
+ # Initialize ivar. +name+ must include the leading '@'.
783
+ def init_ivar(ivar_name, val)
784
+ sym = ivar_name[1..-1].to_sym
785
+ if self.class.subobjects.keys.include?(sym)
786
+ instance_variable_set(ivar_name, self.class.subobjects[sym].new(val))
787
+ elsif self.class.arrays.keys.include?(sym)
788
+ klazz = self.class.arrays[sym]
789
+ val = [val] unless val.kind_of?(Array)
790
+ instance_variable_set(ivar_name, val.collect {|v| v.kind_of?(MongoRecord::Base) ? v : klazz.new(v)})
791
+ else
792
+ instance_variable_set(ivar_name, val)
793
+ end
794
+ end
795
+
796
+ def set_create_times(t=nil)
797
+ t ||= Time.now
798
+ self.class.field_names.each { |iv|
799
+ case iv
800
+ when :created_at
801
+ instance_variable_set("@#{iv}", t)
802
+ when :created_on
803
+ instance_variable_set("@#{iv}", Time.local(t.year, t.month, t.day))
804
+ end
805
+ }
806
+ self.class.subobjects.keys.each { |iv|
807
+ val = instance_variable_get("@#{iv}")
808
+ val.send(:set_create_times, t) if val
809
+ }
810
+ end
811
+
812
+ def set_update_times(t=nil)
813
+ t ||= Time.now
814
+ self.class.field_names.each { |iv|
815
+ case iv
816
+ when :updated_at
817
+ instance_variable_set("@#{iv}", t)
818
+ when :updated_on
819
+ instance_variable_set("@#{iv}", Time.local(t.year, t.month, t.day))
820
+ end
821
+ }
822
+ self.class.subobjects.keys.each { |iv|
823
+ val = instance_variable_get("@#{iv}")
824
+ val.send(:set_update_times, t) if val
825
+ }
826
+ end
827
+
828
+ end
829
+
830
+ end