georgepalmer-couch_foo 0.7.1

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,2117 @@
1
+ require 'digest/md5'
2
+ require 'couchrest'
3
+
4
+ module CouchFoo
5
+
6
+ # Generic Active Record exception class.
7
+ class CouchFooError < StandardError
8
+ end
9
+
10
+ # Raised when the inheritance mechanism failes to locate the subclass
11
+ # (for example due to improper usage of column that +inheritance_column+ points to).
12
+ class SubclassNotFound < CouchFooError #:nodoc:
13
+ end
14
+
15
+ # Raised when attribute has a name reserved by CouchFoo
16
+ class DangerousAttributeError < CouchFooError
17
+ end
18
+
19
+ # Raised when Couch Foo cannot find the document by given id or set of ids.
20
+ class DocumentNotFound < CouchFooError
21
+ end
22
+
23
+ # Raised by save! when record cannot be saved because record is invalid
24
+ class DocumentNotSaved < CouchFooError
25
+ end
26
+
27
+ # Raised on attempt to update record that is instantiated as read only.
28
+ class ReadOnlyRecord < CouchFooError
29
+ end
30
+
31
+ # Raised when attempting to update an old revision of a document
32
+ class DocumentConflict < CouchFooError
33
+ end
34
+
35
+ # Simple class encapsulating a property
36
+ class Property
37
+ attr_accessor :name, :type, :default
38
+ def initialize(name, type, default = nil, *args)
39
+ self.name = name
40
+ self.type = type
41
+ self.default = default
42
+ end
43
+ end
44
+
45
+ # Simple class encapsulating a view
46
+ class View
47
+ attr_accessor :name, :map_function, :reduce_function, :options
48
+ def initialize(name, map_function, reduce_function, options, *args)
49
+ self.name = name
50
+ self.map_function = map_function
51
+ self.reduce_function = reduce_function
52
+ self.options = options
53
+ end
54
+ end
55
+
56
+ # == Introduction
57
+ #
58
+ # CouchDB (http://couchdb.apache.org/) works slightly differently to relational databases. First,
59
+ # and foremost, it is a document-orientated database. That is, data is stored in documents each
60
+ # of which have a unique id that is used to access and modify it. The contents of the documents
61
+ # are free from structure (or schema free) and bare no relation to one another (unless you encode
62
+ # that within the documents themselves). So in many ways documents are like records within a
63
+ # relational database except there are no tables to keep documents of the same type in.
64
+ #
65
+ # CouchDB interfaces with the external world via a RESTful interface. This allows document
66
+ # creation, updating, deletion etc. The contents of a document are specified in JSON so its
67
+ # possible to serialise objects with the database record efficiently as well as store all the
68
+ # normal types natively.
69
+ #
70
+ # As a consequence of its free form structure there is no SQL to query the database. Instead you
71
+ # define (table-oriented) views that emit certain bits of data from the record and apply
72
+ # conditions, sorting etc to those views. For example if you were to emit the colour attribute
73
+ # you could find all documents with a certain colour. This is similar to indexed lookups on a
74
+ # relational table (both in terms of concept and performance).
75
+ #
76
+ # CouchDB has been designed from the ground up to operate in a distributed way. It provides
77
+ # robust, incremental replication with bi-directional conflict detection and resolution. It's an
78
+ # excellent choice for unstructed data, large datasets that need sharding efficiently and situations
79
+ # where you wish to run local copies of the database (for example in satellite offices).
80
+ #
81
+ # If using CouchDB in a more traditional database sense, it is common to specify a class attribute
82
+ # in the document so that a view can be defined to find all documents of that class. This is
83
+ # similar to a relational database table. CouchFoo defines a ruby_class property that holds the
84
+ # class name of the model that it's representing. It is then possible to do User.all for example.
85
+ # As such it has been possible, with a few minor exceptions, to take the interface to ActiveRecord
86
+ # and re-implement it for CouchDB. This should allow for easy migration from MySQL and friends
87
+ # to CouchDB. Further, CouchFoo, has been designed to take advantages of some features that
88
+ # CouchDB offers that are not available in relational databases. For example - multiple updates
89
+ # and efficient sharding of data
90
+ #
91
+ # In terms of performance CouchDB operates differently in some areas when compared to relationsal
92
+ # databases. That's not to say it's better or worse, just different - you pay the price for
93
+ # certain operations at different points. As such there's a performance section below that
94
+ # details which areas are better performing and which worse. This is something to be aware when
95
+ # writing or migrating applications and using CouchDB.
96
+ #
97
+ # == Quick start guide - what's different?
98
+ #
99
+ # This section outlines differences between ActiveRecord and CouchFoo and a few specific CouchDB
100
+ # points to be aware of
101
+ #
102
+ # As CouchDB is schema-free there are no migrations and no schemas to worry about. You simply
103
+ # specify properties inside the model (a bit like in DataMapper) and add/subtract from them as
104
+ # you need. Adding new properties initializes that attribute to nil when using a document that
105
+ # doesn't have the corresponding data and removing a field makes that attribute no longer available
106
+ # (and removes it from the record on next save). You can optionally specify a type with a
107
+ # property although it makes sense to do so if you can (it'll convert to that type). For example:
108
+ #
109
+ # class Address < CouchFoo::Base
110
+ # property :number, Integer
111
+ # property :street, String
112
+ # property :postcode # Any generic type is fine as long as .to_json can be called on it
113
+ # end
114
+ #
115
+ # Documents have three more properties that get added automatically. _id and _rev are CouchDB
116
+ # internal properties. The _id is the document UUID and never changes. This is created by the
117
+ # gem to be unique accross mulitple computers so should never result in a conflict. It is also
118
+ # aliased to id to ensure compatability with ActiveRecord. The _rev attribute is changed by
119
+ # CouchDB each time an update is made. It is used internally by CouchDB to detect conflicts. The
120
+ # final property is ruby_class that is used by CouchFoo to determine which documents map to which
121
+ # models.
122
+ #
123
+ # CouchDB has a concept called bulk saving where mutliple operations can be built up and commited
124
+ # at once. This has the added advantage of being in a transaction so all or none of the operations
125
+ # will complete. By default bulk saving is switched off but you can set the
126
+ # CouchFoo::Base.bulk_save_default option or override on an individual database basis if you
127
+ # wish to use this feature. If using bulk_saving there are good performance gains when updating
128
+ # large amounts of documents but beaware it is the developers responsability to commit the work.
129
+ # For example:
130
+ #
131
+ # User.all.size # => 27
132
+ # User.create(:name => "george")
133
+ # User.all.size # => 27
134
+ # User.database.commit # or you can use self.class.database.commit if just using 1 database
135
+ # User.all.size # => 28
136
+ #
137
+ # If using this option in rails it would be a good idea to specify an after_filter so that any
138
+ # changes are commited at the end of an action. If you are sharding database across several
139
+ # databases you will need to deal with this in the after_filter logic as well.
140
+ #
141
+ # Conflicts occur when a copy of the document is altered under CouchFoo. That is since loading
142
+ # the document into memory another person or program has altered its contents. CouchDB detects
143
+ # this through the _rev attribute. If a conflict occurs a DocumentConflict error is raised. This
144
+ # is effectively the same as ActiveRecord Optimistic locking mechanism. Exceptions should be
145
+ # dealt with in application logic.
146
+ #
147
+ # On the finders and associations there are a few options that are no longer relevant - :select,
148
+ # :joins, :having, :group, :from and :lock Everything else is available although :conditions and
149
+ # :order work slightly differently and :include hasn't been implemented yet. :conditions is now
150
+ # only a hash, eg :conditions => {:user_name => user_name} and doesn't accept an array or SQL.
151
+ # :offset or :skip is quite inefficient so should be used sparingly :order isn't a SQL fragement
152
+ # but a field to sort the results on - eg :order => :created_at At the moment this can only be
153
+ # one field so it's best to sort the results yourself once you've got them back if you require
154
+ # complex ordering. As the order is applied once the results are retrieved from the database it
155
+ # cannot be used with :limit. The reason :order is applied after the results are retrieved is
156
+ # CouchDB can only order on items exposed in the key. Thus if you wanted to sort User.all 5
157
+ # different ways throughout your application you would need 5 indexes rather than one. This is
158
+ # quite inefficient so ordering is performed once the data is retrieved from the database with
159
+ # the cost that using :order with :limit has to be performed an alternate way.
160
+ #
161
+ # When using finders views are automatically built by CouchFoo for the model. For example, with
162
+ # the class House, the first House.find(:id) will create a House design document with a view
163
+ # that exposes the id of the house as the key - any house can then be selected on this. If you
164
+ # then do a query for a certain colour house, ie House.find_all_by_colour("red") then a view that
165
+ # exposes the house colour as the key will be added to the design document. This works well when
166
+ # using conditions (as we can generate the key from the conditions used) but means it's not
167
+ # possible to find the oldest 5 users in the system if :created_at isn't exposed in the key. As
168
+ # such it is possible to set the key to use with a view directly. For example:
169
+ #
170
+ # Person.find(:first, :use_key => :created_at) # Finds earliest person as results sorted on key
171
+ # Person.find(:all, :use_key => [:name, :category], :conditions => {:category => "Article"}, :limit => 50) # Finds 50 people with a category of "Article" sorted by name
172
+ #
173
+ # Note in the second case that we must use the condition keys in the :use_key array. Both of
174
+ # these will get the desired results but at the expense of creating a new index. For more complex
175
+ # queries it is also possible to specify your own map and reduce functions using the CouchFoo#view
176
+ # call. See the CouchDB view documentation and the CouchFoo#view documentation for more on this
177
+ #
178
+ # Using relational databases with incrementing keys we have become accustom to adding a new record
179
+ # and then using find(:last) to retrieve it. As each document in CouchDB has a unique identifier
180
+ # this is no longer the case. This is particularly important when creating interfaces as it is
181
+ # normal to add items to the bottom of lists and expect on reload for the order to be maintained.
182
+ # This will not happen with CouchDB. As such it is recommend to use the CouchFoo#default_sort
183
+ # macro that applies a default sort order to the model each time it's retrieved from the database.
184
+ # This way you can set default_sort :created_at and not worry about hitting the problem again.
185
+ #
186
+ # With CouchDB the price to pay for inserting data into an indexed view isn't paid at insertion
187
+ # time like MySQL and friends, but at the point of next retrieving that view (although it's
188
+ # possible to override this in order to sacrifice performance for accuracy). As such you can
189
+ # either pay the performance cost when accessing the view (which might be ok if the view gets
190
+ # used a lot but not if a million applicable documents have been added since the last view) or
191
+ # add an external process which gets called everytime a document is created in CouchDB. More of
192
+ # this is outlined in the CouchFoo#find documentation
193
+ #
194
+ # Connecting to the database is done in a way similar to ActiveRecord where the database is
195
+ # inherited by each subclass unless overridden. In CouchFoo you use the call set_database method
196
+ # with a URL and CouchDB version if you're not on edge (this gem supports 0.8 and 0.9 edge). As
197
+ # there's no database connection to be maintained sharding data is very efficient and some
198
+ # applications even use one database per user.
199
+ #
200
+ # If using rails, for now you need to specify your own initializer to make the default database
201
+ # connection. This will be brought inline with rails in the future, using a couchdb.yml
202
+ # configuration file or similar. But for now an initializer file in config/initializers like the
203
+ # follwing will do the trick:
204
+ #
205
+ # CouchFoo::Base.set_database("http://localhost:5984/opnli_dev", 0.8)
206
+ #
207
+ # A few tidbits:
208
+ # * When specifying associations you still need to specify the object_id and object_type (if using polymorphic association) properties. We have this automated as part of the association macro soon
209
+ # * validates_uniqueness_of has had the :case_sensitive option dropped
210
+ # * Because there's no SQL there's no SQL finder methods but equally there's no SQL injection to worry about. You should still sanitize output when displaying back to the user though because you will still be vunerable to JS attacks etc.
211
+ # * Some operations are more efficient than relational databases, others less so. See the performance section for more details on this
212
+ # * Every so often the database will need compacting to recover space lost to old document revisions. See http://wiki.apache.org/couchdb/Compaction for more details on this.
213
+ #
214
+ # The following things are not in this implementation (but are in ActiveRecord):
215
+ # * :include - Although the finders and associations allow this option the actual implementation is yet to be written
216
+ # * Timezones
217
+ # * Aggregations
218
+ # * Fixtures and general testing support
219
+ # * Query cache
220
+ #
221
+ # == Properties
222
+ #
223
+ # Couch Foo objects specify their properties through the use of property definitions inside the
224
+ # model itself. This is unlike ActiveRecord (but similar to DataMapper). As the underlying
225
+ # database document is stored in JSON (String, Integer, Float, Time, DateTime, Date and
226
+ # Boolean/TrueClass are the available types) there are a few differences between CouchFoo and
227
+ # ActiveRecord. The following table highlights any changes that will need to be made to
228
+ # model types:
229
+ #
230
+ # ActiveRecord type | CouchFoo type
231
+ # ------------------+---------------------------------
232
+ # #Text | String
233
+ # #Decimal | Float
234
+ # #Timestamp | Time
235
+ # #Binary | Attachment (not yet implemented)
236
+ #
237
+ # An example of a model is as follows:
238
+ #
239
+ # class Address < CouchFoo::Base
240
+ # property :number, Integer
241
+ # property :street, String
242
+ # property :postcode # Any generic type is fine as long as .to_json can be called on it
243
+ # end
244
+ #
245
+ # == Creation
246
+ #
247
+ # Couch Foo accept constructor parameters either in a hash or as a block. The hash method is
248
+ # especially useful when you're receiving the data from somewhere else, like an HTTP request.
249
+ # It works like this:
250
+ #
251
+ # user = User.new(:name => "David", :occupation => "Code Artist")
252
+ # user.name # => "David"
253
+ #
254
+ # You can also use block initialization:
255
+ #
256
+ # user = User.new do |u|
257
+ # u.name = "David"
258
+ # u.occupation = "Code Artist"
259
+ # end
260
+ #
261
+ # And of course you can just create a bare object and specify the attributes after the fact:
262
+ #
263
+ # user = User.new
264
+ # user.name = "David"
265
+ # user.occupation = "Code Artist"
266
+ #
267
+ # == Conditions
268
+ #
269
+ # Conditions are specified as a Hash. This is different from ActiveRecord where they can be
270
+ # specified as a String, Array or Hash. Example:
271
+ #
272
+ # class User < ActiveRecord::Base
273
+ # def self.authenticate_safely_simply(user_name, password)
274
+ # find(:first, :conditions => { :user_name => user_name, :password => password })
275
+ # end
276
+ # end
277
+ #
278
+ # A range may be used in the hash to find documents between two values:
279
+ #
280
+ # Student.find(:all, :conditions => { :grade => 9..12 })
281
+ #
282
+ # A startkey or endkey can be used to find documents where the documents exceed or preceed the
283
+ # value. A key is needed here so CouchFoo knows which property to apply the startkey to:
284
+ #
285
+ # Student.find(:all, :use_key => :grade, :startkey => 80)
286
+ #
287
+ # Finally an array may be used in the hash to use find records just matching those values. This
288
+ # operation requires CouchDB > 0.8 though:
289
+ #
290
+ # Student.find(:all, :conditions => { :grade => [9,11,12] })
291
+ #
292
+ # == Overwriting default accessors
293
+ #
294
+ # All column values are automatically available through basic accessors on the Active Record object, but sometimes you
295
+ # want to specialize this behavior. This can be done by overwriting the default accessors (using the same
296
+ # name as the attribute) and calling <tt>read_attribute(attr_name)</tt> and <tt>write_attribute(attr_name, value)</tt> to actually change things.
297
+ # Example:
298
+ #
299
+ # class Song < ActiveRecord::Base
300
+ # # Uses an integer of seconds to hold the length of the song
301
+ #
302
+ # def length=(minutes)
303
+ # write_attribute(:length, minutes.to_i * 60)
304
+ # end
305
+ #
306
+ # def length
307
+ # read_attribute(:length) / 60
308
+ # end
309
+ # end
310
+ #
311
+ # You can alternatively use <tt>self[:attribute]=(value)</tt> and <tt>self[:attribute]</tt> instead of <tt>write_attribute(:attribute, value)</tt> and
312
+ # <tt>read_attribute(:attribute)</tt> as a shorter form.
313
+ #
314
+ # == Attribute query methods
315
+ #
316
+ # In addition to the basic accessors, query methods are also automatically available on the Active Record object.
317
+ # Query methods allow you to test whether an attribute value is present.
318
+ #
319
+ # For example, an Active Record User with the <tt>name</tt> attribute has a <tt>name?</tt> method that you can call
320
+ # to determine whether the user has a name:
321
+ #
322
+ # user = User.new(:name => "David")
323
+ # user.name? # => true
324
+ #
325
+ # anonymous = User.new(:name => "")
326
+ # anonymous.name? # => false
327
+ #
328
+ # == Accessing attributes before they have been typecasted
329
+ #
330
+ # Sometimes you want to be able to read the raw attribute data without having the property typecast run its course first.
331
+ # That can be done by using the <tt><attribute>_before_type_cast</tt> accessors that all attributes have. For example, if your Account model
332
+ # has a <tt>balance</tt> attribute, you can call <tt>account.balance_before_type_cast</tt> or <tt>account.id_before_type_cast</tt>.
333
+ #
334
+ # This is especially useful in validation situations where the user might supply a string for an integer field and you want to display
335
+ # the original string back in an error message. Accessing the attribute normally would typecast the string to 0, which isn't what you
336
+ # want.
337
+ #
338
+ # == Dynamic attribute-based finders
339
+ #
340
+ # Dynamic attribute-based finders are a cleaner way of getting (and/or creating) objects by simple queries. They work by
341
+ # appending the name of an attribute to <tt>find_by_</tt> or <tt>find_all_by_</tt>, so you get finders like <tt>Person.find_by_user_name</tt>,
342
+ # <tt>Person.find_all_by_last_name</tt>, and <tt>Payment.find_by_transaction_id</tt>. So instead of writing
343
+ # <tt>Person.find(:first, :conditions => {:user_name => user_name})</tt>, you just do <tt>Person.find_by_user_name(user_name)</tt>.
344
+ # And instead of writing <tt>Person.find(:all, :conditions => {:last_name => last_name})</tt>, you just do <tt>Person.find_all_by_last_name(last_name)</tt>.
345
+ #
346
+ # It's also possible to use multiple attributes in the same find by separating them with "_and_", so you get finders like
347
+ # <tt>Person.find_by_user_name_and_password</tt> or even <tt>Payment.find_by_purchaser_and_state_and_country</tt>. So instead of writing
348
+ # <tt>Person.find(:first, :conditions => {:user_name => user_name, :password => password})</tt>, you just do
349
+ # <tt>Person.find_by_user_name_and_password(user_name, password)</tt>.
350
+ #
351
+ # It's even possible to use all the additional parameters to find. For example, the full interface for <tt>Payment.find_all_by_amount</tt>
352
+ # is actually <tt>Payment.find_all_by_amount(amount, options)</tt>. And the full interface to <tt>Person.find_by_user_name</tt> is
353
+ # actually <tt>Person.find_by_user_name(user_name, options)</tt>. So you could call <tt>Payment.find_all_by_amount(50, :order => :created_at)</tt>.
354
+ #
355
+ # The same dynamic finder style can be used to create the object if it doesn't already exist. This dynamic finder is called with
356
+ # <tt>find_or_create_by_</tt> and will return the object if it already exists and otherwise creates it, then returns it. Protected
357
+ # attributes won't be set unless they are given in a block. For example:
358
+ #
359
+ # # No 'Summer' tag exists
360
+ # Tag.find_or_create_by_name("Summer") # equal to Tag.create(:name => "Summer")
361
+ #
362
+ # # Now the 'Summer' tag does exist
363
+ # Tag.find_or_create_by_name("Summer") # equal to Tag.find_by_name("Summer")
364
+ #
365
+ # # Now 'Bob' exist and is an 'admin'
366
+ # User.find_or_create_by_name('Bob', :age => 40) { |u| u.admin = true }
367
+ #
368
+ # Use the <tt>find_or_initialize_by_</tt> finder if you want to return a new record without saving it first. Protected attributes
369
+ # won't be setted unless they are given in a block. For example:
370
+ #
371
+ # # No 'Winter' tag exists
372
+ # winter = Tag.find_or_initialize_by_name("Winter")
373
+ # winter.new_record? # true
374
+ #
375
+ # To find by a subset of the attributes to be used for instantiating a new object, pass a hash instead of
376
+ # a list of parameters. For example:
377
+ #
378
+ # Tag.find_or_create_by_name(:name => "rails", :creator => current_user)
379
+ #
380
+ # That will either find an existing tag named "rails", or create a new one while setting the user that created it.
381
+ #
382
+ # == Saving arrays, hashes, and other non-mappable objects in text columns
383
+ #
384
+ # CouchFoo will try and serialize any types that are not specified in properties by calling .to_json on the object
385
+ # This means its not only possible to store arrays and hashes (with no extra work required) but also any generic type
386
+ # you care to define, so long as it has a .to_json method defined. Example:
387
+ #
388
+ # class User < CouchFoo::Base
389
+ # property :name, String
390
+ # property :house # Can be any object so long as it responds to .to_json
391
+ # end
392
+ #
393
+ # == Inheritance
394
+ #
395
+ # Couch Foo allows inheritance by storing the name of the class in a column that by default is named "type" (can be changed
396
+ # by overwriting <tt>Base.inheritance_column</tt>). This means that an inheritance looking like this:
397
+ #
398
+ # class Company < ActiveRecord::Base; end
399
+ # class Firm < Company; end
400
+ # class Client < Company; end
401
+ # class PriorityClient < Client; end
402
+ #
403
+ # When you do <tt>Firm.create(:name => "37signals")</tt>, this record will be saved in the companies table with type = "Firm". You can then
404
+ # fetch this row again using <tt>Company.find(:first, {:name => "37signals"})</tt> and it will return a Firm object.
405
+ #
406
+ # If you don't have a type column defined in your table, inheritance won't be triggered. In that case, it'll work just
407
+ # like normal subclasses with no special magic for differentiating between them or reloading the right type with find.
408
+ #
409
+ # == Connection to multiple databases in different models
410
+ #
411
+ # Connections are usually created through CouchFoo::Base.set_database and retrieved by CouchFoo::Base.database
412
+ # All classes inheriting from CouchFoo::Base will use this connection. But you can also set a class-specific connection.
413
+ # For example, if Course is an CouchFoo::Base, but resides in a different database, you can just say <tt>Course.set_database url</tt>
414
+ # and Course and all of its subclasses will use this connection instead.
415
+ #
416
+ # == Performance
417
+ #
418
+ # CouchDB operates via a RESTful interface and so isn't as efficient as a local database when
419
+ # running over a local socket. This is due to the TCP/IP overhead. But given any serious
420
+ # application runs a separate database and application server then it is unfair to judge
421
+ # CouchDB on this alone.
422
+ #
423
+ # Generally speaking CouchDB performs as well as or better than relational databases when it already
424
+ # has the document in memory. If it doesn't it performs worse as it must first find the document
425
+ # and submit the new version (as there's no structure to documents it can't update fields on an
426
+ # add-hoc basis, it must have the whole document). This makes class operations such as update_all
427
+ # less efficient. If your application is high load and uses these excessively you may wish to
428
+ # consider other databases. On the flip side if you have lots of documents in memory and wish to
429
+ # update them all, using bulk_save is an excellent way to make performance gains.
430
+ #
431
+ # Below is a list of operations where the performance differs from ActiveRecord. More notes are
432
+ # available in the functions themselves:
433
+ #
434
+ # * class.find - when using list of ids is O(n) rather than O(1) if not on CouchDB 0.9
435
+ # * class.create - O(1) rather than O(n) if using bulk_save
436
+ # * class.delete - O(n) rather than O(1) so less efficient for > 1 document
437
+ # * class.update_all - O(n+1) rather than O(1)
438
+ # * class.delete_all - O(2n) rather than O(1)
439
+ # * class.update_counters - O(2) rather than O(1)
440
+ # * class.increment_counter - O(2) rather than O(1)
441
+ # * class.decrement_counter - O(2) rather than O(1)
442
+ # * save, save!, update_attribute, update_attributes, update_attributes!, increment!, decrement!,
443
+ # toggle! - if using bulk_save then O(1) rather than O(n)
444
+
445
+ class Base
446
+ # Accepts a logger conforming to the interface of Log4r or the default Ruby 1.8+ Logger class,
447
+ # which is then passed on to any new database connections made and which can be retrieved on
448
+ # both a class and instance level by calling +logger+.
449
+ cattr_accessor :logger, :instance_writer => false
450
+
451
+ # Accessor for the name of the prefix string to prepend to every document name. So if set to
452
+ # "basecamp_", all document names will be named like "basecamp_project", "basecamp_person", etc.
453
+ # This is a convenient way of creating a namespace for documents in a shared database. By
454
+ # default, the prefix is the empty string.
455
+ cattr_accessor :document_name_prefix, :instance_writer => false
456
+ @@document_name_prefix = ""
457
+
458
+ # Works like +document_name_prefix+, but appends instead of prepends (set to "_basecamp" gives
459
+ # "projects_basecamp", "people_basecamp"). By default, the suffix is the empty string.
460
+ cattr_accessor :document_name_suffix, :instance_writer => false
461
+ @@document_name_suffix = ""
462
+
463
+ # Properties that cannot be altered by the user. By default this includes _id, _rev (both
464
+ # CouchDB internals) and ruby_class (used by CouchFoo to match a CouchDB document to a ruby
465
+ # class)
466
+ cattr_accessor :unchangeable_property_names, :instance_writer => false
467
+ @@unchangeable_property_names = [:_id, :_rev, :ruby_class]
468
+
469
+ # Determines whether to use Time.local (using :local) or Time.utc (using :utc) when pulling dates
470
+ # and times from the database. This is set to :local by default.
471
+ cattr_accessor :default_timezone, :instance_writer => false
472
+ @@default_timezone = :local
473
+
474
+ class << self # Class Methods
475
+ # CouchDB has a concept called views which show a subset of documents in the database subject to criteria.
476
+ # The documents selected are chosen according to a map function and an optional reduce function.
477
+ # The later is useful for example, to count all the documents that have been matched by the initial map function.
478
+ # CouchFoo automatically creates views for each of the data models you use as you require them.
479
+ # For example take the class House. The first House.find(:id) will create a House design document with a view
480
+ # that exposes the id of the house as a key - any house can then be selected on this. If you then do a query
481
+ # for a certain colour house, ie House.find_all_by_colour("red") then a view that exposes the house colour as
482
+ # a key will be added to the design document. So as you perform new queries the design document for a model
483
+ # is updated so you are always accessing via an 'indexed' approach. This should be transparent to the developer
484
+ # but the resulting views can be seen by looking up the design document in the database.
485
+ #
486
+ # CouchFoo cannot handle automatic view generation for the case where both an :order and :limit should be
487
+ # applied to a result set. This is because the ordering is performed after retrieving data from CouchDB
488
+ # whereas the limit is applied at the database level. As such if you were limiting to 5 results
489
+ # and ordering on a property you would get the same 5 results in a different order rather than the first 5
490
+ # and last 5 results for that data type. To overcome this restriction either define the property you wish
491
+ # to order on in the :use_key option (make sure you add conditions you're using in here as well) or create your
492
+ # own views using CouchFoo#view. You can then use the :descending => true to reverse the results order.
493
+ #
494
+ # There is a slight caveat to the way views work in CouchDB. The index is only updated each time a view
495
+ # is accessed (although this can be overridden using :update). This is both good and bad. Good in the
496
+ # sense you don't pay the price at insertion time, bad in the sense you pay when accessing the view the
497
+ # next time (although this can be more efficient). The simple solution is to write a script that
498
+ # periodically calls the view to update the index as described on the CouchDB site in this FAQ:
499
+ # http://wiki.apache.org/couchdb/Frequently_asked_questions#update_views_more_often Needless to say
500
+ # creating a new view on a large dataset is expensive but this is no different from creating an index
501
+ # on a large MySQL table (although in general it's a bit slower as all the documents are in one place
502
+ # rather than split into tables)
503
+ #
504
+ # It is possible to perform unindexed queries by using slow views, although this is not recommended
505
+ # for production use. Like MySQL performing unindexed lookups is very inefficient on large datasets.
506
+ #
507
+ # One final point to note is we're used to using relational databases that have auto-incrementing keys.
508
+ # Therefore the newest rows added to the database have the highest key value (some databases go back
509
+ # and fill in the missing/deleted keys after a restart but generally speaking...) and are therefore shown last
510
+ # in lists on the front end. When using CouchDB each item is allocated a UUID which varies massively
511
+ # depending on time, computer IP etc. Therefore it is likely that adding a new item to a page via AJAX
512
+ # will add the item to the bottom of the list but when the page is reloaded it occurs in the middle
513
+ # somewhere. This is very confusing for users so it is therefore recommended that you sort items on a
514
+ # :created_at field by default (see CouchFoo#default_sort).
515
+ #
516
+ # More information can be found on CouchDB views at: http://wiki.apache.org/couchdb/Introduction_to_CouchDB_views
517
+ #
518
+ # CouchFoo operates with four different retrieval approaches:
519
+ #
520
+ # * Find by id - This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]).
521
+ # If no record can be found for all of the listed ids, then DocumentNotFound will be raised. This only
522
+ # accepts the :conditions and :readonly options. Note when using a list/array of ids the lookup is O(n)
523
+ # efficiency rather than O(1) (as with ActiveRecord) if using CouchDB<0.9
524
+ # * Find first - This will return the first record matched by the options used. These options can either be
525
+ # specific conditions or merely an order. If no record can be matched, +nil+ is returned. Use
526
+ # <tt>Model.find(:first, *args)</tt> or its shortcut <tt>Model.first(*args)</tt>. Be aware this will return
527
+ # the first document by UUID and this isn't always what you expect even when using default_sort (see note above)
528
+ # * Find last - This will return the last record matched by the options used. These options can either be
529
+ # specific conditions or merely an order. If no record can be matched, +nil+ is returned. Use
530
+ # <tt>Model.find(:last, *args)</tt> or its shortcut <tt>Model.last(*args)</tt>. Be aware this will return
531
+ # the last document by UUID and this isn't always what you expect even when using default_sort (see note above)
532
+ # * Find all - This will return all the records matched by the options used.
533
+ # If no records are found, an empty array is returned. Use
534
+ # <tt>Model.find(:all, *args)</tt> or its shortcut <tt>Model.all(*args)</tt>.
535
+ #
536
+ # All approaches accept an options hash as their last parameter.
537
+ #
538
+ # ==== Attributes
539
+ #
540
+ # NOTE: Only :conditions and :readonly are available on find by id lookups
541
+ #
542
+ # * <tt>:conditions</tt> - This can only take a Hash of options to match, not SQL fragments
543
+ # like ActiveRecord. For example :conditions => {:size = 6, :price => 30..80} or
544
+ # :conditions => {:size => [6, 8, 10]} <b>Note</b> when using the later approach and
545
+ # specifying a discrete list CouchDB doesn't support ranges in the same query
546
+ # * <tt>:order</tt> - With a field name sorts on that field. This is applied after the results
547
+ # are returned from the database so should not be used with :limit and is fairly pointless
548
+ # with find(:first) and find(:last) types. See CouchFoo#view for how to create views
549
+ # that can be sorted ActiveRecord style
550
+ # * <tt>:include</tt> - not implemented yet
551
+ # * <tt>:limit</tt> - an integer determining the limit on the number of rows that should be
552
+ # returned. Take caution if using with :order (read notes in :order and section header)
553
+ # * <tt>:offset</tt> - An integer determining the offset from where the rows should be fetched.
554
+ # So at 5, it would skip rows 0 through 4. This is the same as :skip listed below in the
555
+ # further options. Note: This is not particulary efficient in CouchDB
556
+ # * <tt>:update</tt> - If set to false will not update the view so although the access will be
557
+ # faster some of the data may be out of date. Recommended if you are managing view updation
558
+ # independently
559
+ # * <tt>:readonly</tt> - Mark the returned documents read-only so they cannot be saved or updated.
560
+ # * <tt>:view_type</tt> - by default views are created for queries where there is no view (this
561
+ # is equivalent to no index on the column in MySQL) to keep lookups efficient. However
562
+ # by passing :view_type => :slow a CouchDB slow query will be performed. As the name suggests
563
+ # these are slow and should only be used in development not production. Be sure to read the
564
+ # note above on how CouchDB indexing works and responsabilites of the developer.
565
+ # * <tt>:use_key</tt> - The key to emit in the view. The key is used for selection and ordering
566
+ # so is a good way to order results and limit to a certain quantity, or to find results that
567
+ # are greater or less than a certain value (in combination with :startkey, :endkey). Normally
568
+ # this value is automatically determined when using :conditions. As such when using in
569
+ # combination with :conditions option this must contain both the items you would like in the key
570
+ # and the items you're using in the conditions. For example:
571
+ # User.find(:all, :use_key => [:name, :administrator], :conditions => {:administrator => true})
572
+ # * <tt>:startkey</tt> - Used to find all documents from this value up, for example
573
+ # User.find(:all, :startkey => 20) This needs to be used with a custom map function where
574
+ # the user has chosen the exposing key for it to be meaningful.
575
+ # * <tt>:endkey</tt> - As :startkey but documents upto that key rather than from it
576
+ # * <tt>:return_json</tt> - If you are emitting something other than the document as the value
577
+ # on a custom map function you may wish to return the raw JSON as instantiating objects may not
578
+ # be possible. Using this option will ignore any :order or :readonly settings
579
+ # * <tt>Further options</tt> - The CouchDB view options :descending, :group, :group_level,
580
+ # :skip, :keys, :startkey_docid and :endkey_docid are supported on views but they
581
+ # are unlikely to be required unless the developer is specifying their own map or reduce function.
582
+ # Note some of these require CouchDB 0.9 (see CouchDB wiki for list)
583
+ #
584
+ # ==== Examples
585
+ #
586
+ # # find by id
587
+ # Person.find(1) # returns the object for ID = 1
588
+ # Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6)
589
+ # Person.find([7, 17]) # returns an array for objects with IDs in (7, 17)
590
+ # Person.find([1]) # returns an array for the object with ID = 1
591
+ # Person.find(1, :conditions => {:administrator => 1})
592
+ #
593
+ # Unlike ActiveRecord order will be maintained on multiple id selection but the operation
594
+ # is not as efficient as there is no multiple get from the database
595
+ #
596
+ # # find first
597
+ # Person.find(:first) # returns the first object fetched by key (so this is unlikely to be
598
+ # the oldest person document in the database)
599
+ # Person.find(:first, :use_key => :created_at) # Finds earliest person but at the expense of
600
+ # creating a new view
601
+ # Person.find(:first, :use_key => :created_at, :startkey => "2009/09/01")) # Finds 1st person
602
+ # since 1st September 2009 but uses the same index as above
603
+ #
604
+ # # find last
605
+ # Person.find(:last) # returns the last object, again may not be what's expected
606
+ # Person.find(:last, :conditions => { :user_name => user_name})
607
+ #
608
+ # # find all
609
+ # Person.find(:all) # returns an array of objects
610
+ # Person.find(:all, :conditions => {:category => "Article"}, :limit => 50)
611
+ # Person.find(:all, :use_key => [:name, :category], :conditions => {:category => "Article"}, :limit => 50)
612
+ # # Creates a name, category index and finds the first 50 people ordered by name with a category of "Article"
613
+ def find(*args)
614
+ options = args.extract_options!
615
+ validate_find_options(options)
616
+ set_readonly_option!(options)
617
+
618
+ case args.first
619
+ when :first then find_initial(options)
620
+ when :last then find_last(options)
621
+ when :all then find_every(options)
622
+ else find_from_ids(args, options)
623
+ end
624
+ end
625
+
626
+ # A convenience wrapper for <tt>find(:first, *args)</tt>. You can pass in all the
627
+ # same arguments to this method as you can to <tt>find(:first)</tt>.
628
+ def first(*args)
629
+ find(:first, *args)
630
+ end
631
+
632
+ # A convenience wrapper for <tt>find(:last, *args)</tt>. You can pass in all the
633
+ # same arguments to this method as you can to <tt>find(:last)</tt>.
634
+ def last(*args)
635
+ find(:last, *args)
636
+ end
637
+
638
+ # This is an alias for find(:all). You can pass in all the same arguments to this method as you can
639
+ # to find(:all)
640
+ def all(*args)
641
+ find(:all, *args)
642
+ end
643
+
644
+ # Checks whether a document exists in the database that matches conditions given. These conditions
645
+ # can either be a key to be found, or a condition to be matched like using CouchFoo#find.
646
+ #
647
+ # ==== Examples
648
+ # Person.exists?('5a1278b3c4e')
649
+ # Person.exists?(:name => "David")
650
+ def exists?(id_or_conditions)
651
+ if (id_or_conditions.is_a?(Hash))
652
+ !find(:first, :conditions => {:ruby_class => document_class_name}.merge(id_or_conditions)).nil?
653
+ else
654
+ !find(id_or_conditions, :conditions => {:ruby_class => document_class_name}).nil? rescue false
655
+ end
656
+ end
657
+
658
+ # Creates an object (or multiple objects) and saves it to the database, if validations pass.
659
+ # The resulting object is returned whether the object was saved successfully to the database or not.
660
+ #
661
+ # The +attributes+ parameter can be either be a Hash or an Array of Hashes. These Hashes describe the
662
+ # attributes on the objects that are to be created.
663
+ #
664
+ # If using bulk save this operation is O(1) rather than O(n) so much more efficient
665
+ #
666
+ # ==== Examples
667
+ # # Create a single new object
668
+ # User.create(:first_name => 'Jamie')
669
+ #
670
+ # # Create an Array of new objects
671
+ # User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }])
672
+ #
673
+ # # Create a single object and pass it into a block to set other attributes.
674
+ # User.create(:first_name => 'Jamie') do |u|
675
+ # u.is_admin = false
676
+ # end
677
+ #
678
+ # # Creating an Array of new objects using a block, where the block is executed for each object:
679
+ # User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }]) do |u|
680
+ # u.is_admin = false
681
+ # end
682
+ def create(attributes = nil, &block)
683
+ if attributes.is_a?(Array)
684
+ attributes.collect { |attr| create(attr, &block) }
685
+ else
686
+ object = new(attributes)
687
+ yield(object) if block_given?
688
+ object.save
689
+ object
690
+ end
691
+ end
692
+
693
+ # Updates an object (or multiple objects) and saves it to the database, if validations pass.
694
+ # The resulting object is returned whether the object was saved successfully to the database or not.
695
+ #
696
+ # ==== Attributes
697
+ #
698
+ # * +id+ - This should be the id or an array of ids to be updated.
699
+ # * +attributes+ - This should be a Hash of attributes to be set on the object, or an array of Hashes.
700
+ #
701
+ # ==== Examples
702
+ #
703
+ # # Updating one record:
704
+ # Person.update('6180e9a0-cdca-012b-14a5-001a921a2bec', { :user_name => 'Samuel', :group => 'expert' })
705
+ #
706
+ # # Updating multiple records:
707
+ # people = { '6180e9a0-cdca-012b-14a5-001a921a2bec' => { "first_name" => "David" }, 'e6f6a870-cdc9-012b-14a3-001a921a2bec' => { "first_name" => "Jeremy" } }
708
+ # Person.update(people.keys, people.values)
709
+ def update(id, attributes)
710
+ if id.is_a?(Array)
711
+ idx = -1
712
+ id.collect { |one_id| idx += 1; update(one_id, attributes[idx]) }
713
+ else
714
+ object = find(id)
715
+ object.update_attributes(attributes)
716
+ object
717
+ end
718
+ end
719
+
720
+ # Delete an object (or multiple objects) where the _id and _rev given match the record. No
721
+ # callbacks are fired off executing so this is an efficient method of deleting documents that
722
+ # don't need cleaning up after or other actions to be taken.
723
+ #
724
+ # This operations is O(n) compared to O(1) so is less efficient than ActiveRecord when deleting
725
+ # more than one document
726
+ #
727
+ # Objects are _not_ instantiated with this method.
728
+ #
729
+ # ==== Attributes
730
+ #
731
+ # * +id+ - Can be either a String or an Array of Strings.
732
+ #
733
+ # ==== Examples
734
+ #
735
+ # # Delete a single object
736
+ # Todo.delete('6180e9a0-cdca-012b-14a5-001a921a2bec', '12345678')
737
+ #
738
+ # # Delete multiple objects
739
+ # ids = ['6180e9a0-cdca-012b-14a5-001a921a2bec', 'e6f6a870-cdc9-012b-14a3-001a921a2bec']
740
+ # revs = ['12345678', '12345679']
741
+ # Todo.delete(ids, revs)
742
+ def delete(id, rev)
743
+ if id.is_a?(Array)
744
+ idx = -1
745
+ id.collect {|i| idx += 1; delete(i, rev[idx])}
746
+ else
747
+ database.delete({"_id" => id, "_rev" => rev})
748
+ true
749
+ end
750
+ end
751
+
752
+ # Destroy an object (or multiple objects) that has the given id. Unlike delete this doesn't
753
+ # require a _rev as the object if found, created from the attributes and then destroyed. As
754
+ # such all callbacks and filters are fired off before the object is deleted. This method is
755
+ # the same in efficiency terms as CouchFoo#delete unlike in ActiveRecord where delete is more
756
+ # efficient
757
+ #
758
+ # ==== Examples
759
+ #
760
+ # # Destroy a single object
761
+ # Todo.destroy('6180e9a0-cdca-012b-14a5-001a921a2bec')
762
+ #
763
+ # # Destroy multiple objects
764
+ # Todo.destroy(['6180e9a0-cdca-012b-14a5-001a921a2bec', 'e6f6a870-cdc9-012b-14a3-001a921a2bec'])
765
+ def destroy(id)
766
+ if id.is_a?(Array)
767
+ id.map { |one_id| destroy(one_id) }
768
+ else
769
+ find(id).destroy
770
+ end
771
+ end
772
+
773
+ # Updates all records with details given if they match a set of conditions supplied. Even though
774
+ # this uses a bulk save and immediately commits it must first find the relevant documents so is
775
+ # O(n+1) rather than O(1)
776
+ #
777
+ # ==== Attributes
778
+ #
779
+ # * +updates+ - A hash of attributes to update
780
+ # * +options+ - As CouchFoo#find. Unlike ActiveRecord :order and :limit cannot be used togther
781
+ # unless via a custom view (see notes in CouchFoo#find)
782
+ #
783
+ # ==== Examples
784
+ #
785
+ # # Update all billing objects with the 3 different attributes given
786
+ # Billing.update_all( :category => 'authorized', :approved => 1, :author => 'David' )
787
+ #
788
+ # # Update records that match our conditions
789
+ # Billing.update_all( {:author = 'David'}, :conditions => {:title => 'Rails'} )
790
+ def update_all(updates, options = {})
791
+ find(:all, options).each {|d| d.update_attributes(updates, true)}
792
+ database.commit
793
+ end
794
+
795
+ # Destroys the records matching +conditions+ by instantiating each record and calling the destroy method.
796
+ # This means at least 2*N database queries to destroy N records, so avoid destroy_all if you are deleting
797
+ # many records. If you want to simply delete records without worrying about dependent associations or
798
+ # callbacks, use the much faster +delete_all+ method instead.
799
+ #
800
+ # ==== Attributes
801
+ #
802
+ # * +conditions+ - Conditions are specified the same way as with +find+ method.
803
+ #
804
+ # ==== Example
805
+ #
806
+ # Person.destroy_all "last_login < '2004-04-04'"
807
+ #
808
+ # This loads and destroys each person one by one, including its dependent associations and before_ and
809
+ # after_destroy callbacks.
810
+ def destroy_all(conditions = nil)
811
+ find(:all, :conditions => conditions).each { |object| object.destroy }
812
+ end
813
+
814
+ # Currently there is no way to do selective delete in CouchDB so this simply defers to
815
+ # CouchFoo#destroy_all for API compatability with ActiveRecord
816
+ #
817
+ # This operations is O(2n) compared to O(1) so much less efficient than ActiveRecord
818
+ def delete_all(conditions = nil)
819
+ destroy_all(conditions)
820
+ end
821
+
822
+ # A generic "counter updater" implementation, intended primarily to be
823
+ # used by increment_counter and decrement_counter, but which may also
824
+ # be useful on its own. Unlike ActiveRecord this does not update the
825
+ # database directly but has to first find the record. Therefore updates
826
+ # require 2 database requests.
827
+ #
828
+ # ==== Attributes
829
+ #
830
+ # * +id+ - The id of the object you wish to update a counter on.
831
+ # * +counters+ - An Array of Hashes containing the names of the fields
832
+ # to update as keys and the amount to update the field by as values.
833
+ #
834
+ # ==== Examples
835
+ #
836
+ # # For the Post with id of '5aef343ab2', decrement the comment_count by 1, and
837
+ # # increment the action_count by 1
838
+ # Post.update_counters 'aef343ab2', :comment_count => -1, :action_count => 1
839
+ def update_counters(id, counters)
840
+ record = find(id)
841
+ counters.each do |key,value|
842
+ record.increment(key, value)
843
+ end
844
+ record.save
845
+ end
846
+
847
+ # Increment a number field by one, usually representing a count. Unlike ActiveRecord this
848
+ # does not update the database directly but has to first find the record. Therefore updates
849
+ # are O(2) rather than O(1)
850
+ #
851
+ # ==== Attributes
852
+ #
853
+ # * +counter_name+ - The name of the field that should be incremented.
854
+ # * +id+ - The id of the object that should be incremented.
855
+ #
856
+ # ==== Examples
857
+ #
858
+ # # Increment the post_count property for the record with an id of 'aef343ab2'
859
+ # DiscussionBoard.increment_counter(:post_count, 'aef343ab2')
860
+ def increment_counter(counter_name, id)
861
+ update_counters(id, {counter_name => 1})
862
+ end
863
+
864
+ # Decrement a number field by one, usually representing a count. This works the same as
865
+ # increment_counter but reduces the property value by 1 instead of increasing it. Unlike
866
+ # ActiveRecord this does not update the database directly but has to first find the record.
867
+ # Therefore updates are O(2) rather than O(1)
868
+ #
869
+ # ==== Attributes
870
+ #
871
+ # * +counter_name+ - The name of the field that should be decremented.
872
+ # * +id+ - The id of the object that should be decremented.
873
+ #
874
+ # ==== Examples
875
+ #
876
+ # # Decrement the post_count property for the record with an id of 'aef343ab2'
877
+ # DiscussionBoard.decrement_counter(:post_count, 'aef343ab2')
878
+ def decrement_counter(counter_name, id)
879
+ update_counters(id, {counter_name => -1})
880
+ end
881
+
882
+ # Attributes named in this macro are protected from mass-assignment,
883
+ # such as <tt>new(attributes)</tt>,
884
+ # <tt>update_attributes(attributes)</tt>, or
885
+ # <tt>attributes=(attributes)</tt>.
886
+ #
887
+ # Mass-assignment to these attributes will simply be ignored, to assign
888
+ # to them you can use direct writer methods. This is meant to protect
889
+ # sensitive attributes from being overwritten by malicious users
890
+ # tampering with URLs or forms.
891
+ #
892
+ # class Customer < CouchFoo::Base
893
+ # attr_protected :credit_rating
894
+ # end
895
+ #
896
+ # customer = Customer.new("name" => David, "credit_rating" => "Excellent")
897
+ # customer.credit_rating # => nil
898
+ # customer.attributes = { "description" => "Jolly fellow", "credit_rating" => "Superb" }
899
+ # customer.credit_rating # => nil
900
+ #
901
+ # customer.credit_rating = "Average"
902
+ # customer.credit_rating # => "Average"
903
+ #
904
+ # To start from an all-closed default and enable attributes as needed,
905
+ # have a look at +attr_accessible+.
906
+ def attr_protected(*attributes)
907
+ write_inheritable_attribute("attr_protected", Set.new(attributes.map(&:to_s)) + (protected_attributes || []))
908
+ end
909
+
910
+ # Returns an array of all the attributes that have been protected from mass-assignment.
911
+ def protected_attributes # :nodoc:
912
+ read_inheritable_attribute("attr_protected")
913
+ end
914
+
915
+ # Specifies a white list of model attributes that can be set via
916
+ # mass-assignment, such as <tt>new(attributes)</tt>,
917
+ # <tt>update_attributes(attributes)</tt>, or
918
+ # <tt>attributes=(attributes)</tt>
919
+ #
920
+ # This is the opposite of the +attr_protected+ macro: Mass-assignment
921
+ # will only set attributes in this list, to assign to the rest of
922
+ # attributes you can use direct writer methods. This is meant to protect
923
+ # sensitive attributes from being overwritten by malicious users
924
+ # tampering with URLs or forms. If you'd rather start from an all-open
925
+ # default and restrict attributes as needed, have a look at
926
+ # +attr_protected+.
927
+ #
928
+ # class Customer < CouchFoo::Base
929
+ # attr_accessible :name, :nickname
930
+ # end
931
+ #
932
+ # customer = Customer.new(:name => "David", :nickname => "Dave", :credit_rating => "Excellent")
933
+ # customer.credit_rating # => nil
934
+ # customer.attributes = { :name => "Jolly fellow", :credit_rating => "Superb" }
935
+ # customer.credit_rating # => nil
936
+ #
937
+ # customer.credit_rating = "Average"
938
+ # customer.credit_rating # => "Average"
939
+ def attr_accessible(*attributes)
940
+ write_inheritable_attribute("attr_accessible", Set.new(attributes.map(&:to_s)) + (accessible_attributes || []))
941
+ end
942
+
943
+ # Returns an array of all the attributes that have been made accessible to mass-assignment.
944
+ def accessible_attributes # :nodoc:
945
+ read_inheritable_attribute("attr_accessible")
946
+ end
947
+
948
+ # Attributes listed as readonly can be set for a new record, but will be ignored in database
949
+ # updates afterwards
950
+ def attr_readonly(*attributes)
951
+ write_inheritable_attribute("attr_readonly", Set.new(attributes.map(&:to_s)) + (readonly_attributes || []))
952
+ end
953
+
954
+ # Returns an array of all the attributes that have been specified as readonly
955
+ def readonly_attributes
956
+ read_inheritable_attribute("attr_readonly")
957
+ end
958
+
959
+ # Guesses the document name (in forced lower-case) based on the name of the class in the
960
+ # inheritance hierarchy descending directly from CouchFoo::Base. So if the hierarchy looks
961
+ # like: Reply < Message < CouchFoo::Base, then Reply is used as the document name. The
962
+ # rules used to do the guess are handled by the Inflector class in Active Support, which
963
+ # knows almost all common English inflections. You can add new inflections in
964
+ # config/initializers/inflections.rb.
965
+ #
966
+ # Nested classes and enclosing modules are not considered.
967
+ #
968
+ # ==== Example
969
+ #
970
+ # class Invoice < CouchFoo::Base; end;
971
+ # file class document_name
972
+ # invoice.rb Invoice invoice
973
+ #
974
+ # Additionally, the class-level +document_name_prefix+ is prepended and the
975
+ # +document_name_suffix+ is appended. So if you have "myapp_" as a prefix,
976
+ # the document name guess for an Invoice class becomes "myapp_invoices".
977
+ #
978
+ # You can also overwrite this class method to allow for unguessable
979
+ # links, such as a Mouse class with a link to a "mice" document. Example:
980
+ #
981
+ # class Mouse < CouchFoo::Base
982
+ # set_document_name "mice"
983
+ # end
984
+ def document_class_name
985
+ reset_document_class_name
986
+ end
987
+
988
+ def reset_document_class_name
989
+ name = self.name
990
+ unless self == base_class
991
+ name = superclass.document_class_name
992
+ end
993
+
994
+ doc_class_name = "#{document_name_prefix}#{name}#{document_name_suffix}"
995
+ set_document_class_name(doc_class_name)
996
+ doc_class_name
997
+ end
998
+
999
+ # Sets the document class name to use to the given value, or (if the value
1000
+ # is nil or false) to the value returned by the given block.
1001
+ #
1002
+ # class Project < CouchFoo::Base
1003
+ # set_document_class_name "project"
1004
+ # end
1005
+ def set_document_class_name(value = nil, &block)
1006
+ define_attr_method :document_class_name, value, &block
1007
+ end
1008
+ alias :document_class_name= :set_document_class_name
1009
+
1010
+ # Defines the propety name for use with single table inheritance
1011
+ # -- can be set in subclasses like so: self.inheritance_column = "type_id"
1012
+ def inheritance_column
1013
+ @inheritance_column ||= "type".freeze
1014
+ end
1015
+
1016
+ # Sets the name of the inheritance column to use to the given value,
1017
+ # or (if the value # is nil or false) to the value returned by the
1018
+ # given block.
1019
+ #
1020
+ # class Project < CouchFoo::Base
1021
+ # set_inheritance_column do
1022
+ # original_inheritance_column + "_id"
1023
+ # end
1024
+ # end
1025
+ def set_inheritance_column(value = nil, &block)
1026
+ define_attr_method :inheritance_column, value, &block
1027
+ end
1028
+ alias :inheritance_column= :set_inheritance_column
1029
+
1030
+ # Set a property for the document. These can be passed a type and options hash. If no type
1031
+ # is passed a #to_json method is called on the ruby object and the result stored in the
1032
+ # database. If a type is passed then the object is cast before storing in the database. This
1033
+ # does not guarantee that the object is the correct type (use the validaters for that), it
1034
+ # merely tries to convert the current type to the desired one - for example:
1035
+ # '123' => 123 # useful
1036
+ # 'a' => 0 # probably not desired behaviour
1037
+ # The later would fail with a validator
1038
+ #
1039
+ # The options hash supports:
1040
+ # default - the default value for the attribute to be initalized to
1041
+ #
1042
+ # ==== Example:
1043
+ #
1044
+ # class Invoice < CouchFoo::Base
1045
+ # property :number, Integer
1046
+ # property :paid, TrueClass, :default => false
1047
+ # property :notes, String
1048
+ # property :acl
1049
+ # end
1050
+ def property(name, type = nil, options = {})
1051
+ logger.warn("Using type as a column name may issue unexpected behaviour") if name == :type
1052
+ properties.delete_if{|e| e.name == name} # Subset properties override
1053
+ properties << Property.new(name, type, options[:default])
1054
+ end
1055
+
1056
+ # Returns all properties defined on this class
1057
+ def properties
1058
+ if @properties.nil?
1059
+ @properties = Set.new
1060
+ @properties.merge(superclass.properties) unless self == base_class
1061
+ @properties
1062
+ else
1063
+ @properties
1064
+ end
1065
+ end
1066
+
1067
+ # Returns a hash of property name to types
1068
+ def property_types
1069
+ @properties_type ||= properties.inject({}) do |types, property|
1070
+ types[property.name] = property.type
1071
+ types
1072
+ end
1073
+ end
1074
+
1075
+ # Returns an array of property names
1076
+ def property_names
1077
+ @property_names ||= properties.map { |property| property.name }
1078
+ end
1079
+
1080
+ # Resets all the cached information about properties, which will cause them to be reloaded on
1081
+ # the next request.
1082
+ def reset_property_information
1083
+ generated_methods.each { |name| undef_method(name) }
1084
+ @property_names = @properties = @property_types = @generated_methods = @inheritance_column = nil
1085
+ end
1086
+
1087
+ # True if this isn't a concrete subclass needing a inheritence type condition.
1088
+ def descends_from_couch_foo?
1089
+ if superclass.abstract_class?
1090
+ superclass.descends_from_couch_foo?
1091
+ else
1092
+ superclass == Base
1093
+ end
1094
+ end
1095
+
1096
+ # Sets an order which all queries to this model will be sorted by unless overriden in the finder
1097
+ # This is useful for setting a created_at sort field by default so results are automatically
1098
+ # sorted in the order they were added to the database. NOTE - this sorts after the results
1099
+ # are returned so will not give expected behaviour when using limits or find(:first), find(:last)
1100
+ # For example,
1101
+ # class User < CouchFoo::Base
1102
+ # property :name, String
1103
+ # property :created_at, DateTime
1104
+ # default_sort :created_at
1105
+ # end
1106
+ def default_sort(property)
1107
+ @default_sort_order = property
1108
+ end
1109
+
1110
+ def default_sort_order
1111
+ @default_sort_order
1112
+ end
1113
+
1114
+ # Create a view and return the documents associated with that view. It requires a name,
1115
+ # find_function and optional reduce function (see http://wiki.apache.org/couchdb/HTTP_view_API).
1116
+ # At the moment this function assumes you're going to emit a doc as the value (required to rebuild
1117
+ # the model after running the query)
1118
+ #
1119
+ # For example:
1120
+ # class Note
1121
+ # view :latest_submissions, "function(doc) {if(doc.ruby_class == 'Note') {emit([doc.created_at , doc.note], doc); } }", nil, :descending => true
1122
+ # ...
1123
+ # end
1124
+ #
1125
+ # This example would be an effective way to get the latest notes sorted by create date and note
1126
+ # contents. The above view could then be called:
1127
+ # Note.latest_submissions(:limit => 5)
1128
+ #
1129
+ # NOTE: We use descending => true and not order as order is applied after the results are retrieved
1130
+ # from CouchDB whereas descending is a CouchDB view option. More on this can be found in the #find
1131
+ # documentation
1132
+ # NOTE: Custom views do not worked with named scopes, any desired scopes should be coded
1133
+ # into the map function
1134
+ def view(name, map_function, reduce_function = nil, standard_options = {})
1135
+ views << View.new(name, map_function, reduce_function, standard_options)
1136
+ end
1137
+
1138
+ def views
1139
+ @views ||= Set.new()
1140
+ end
1141
+
1142
+ def view_names
1143
+ @view_names ||= views.map{ |view| view.name }
1144
+ end
1145
+
1146
+ def inspect
1147
+ if self == Base
1148
+ super
1149
+ elsif abstract_class?
1150
+ "#{super}(abstract)"
1151
+ else
1152
+ attr_list = properties.map { |p| "#{p.name}: #{p.type || 'JSON'}" } * ', '
1153
+ "#{super}(#{attr_list})"
1154
+ end
1155
+ end
1156
+
1157
+ # Log and benchmark multiple statements in a single block. Example:
1158
+ #
1159
+ # Project.benchmark("Creating project") do
1160
+ # project = Project.create("name" => "stuff")
1161
+ # project.create_manager("name" => "David")
1162
+ # project.milestones << Milestone.find(:all)
1163
+ # end
1164
+ #
1165
+ # The benchmark is only recorded if the current level of the logger is less than or equal
1166
+ # to the <tt>log_level</tt>, which makes it easy to include benchmarking statements in
1167
+ # production software that will remain inexpensive because the benchmark will only be
1168
+ # conducted if the log level is low enough.
1169
+ #
1170
+ # The logging of the multiple statements is turned off unless <tt>use_silence</tt> is set
1171
+ # to false.
1172
+ def benchmark(title, log_level = Logger::DEBUG, use_silence = true)
1173
+ if logger && logger.level <= log_level
1174
+ result = nil
1175
+ seconds = Benchmark.realtime { result = use_silence ? silence { yield } : yield }
1176
+ logger.add(log_level, "#{title} (#{'%.5f' % seconds})")
1177
+ result
1178
+ else
1179
+ yield
1180
+ end
1181
+ end
1182
+
1183
+ # Silences the logger for the duration of the block.
1184
+ def silence
1185
+ old_logger_level, logger.level = logger.level, Logger::ERROR if logger
1186
+ yield
1187
+ ensure
1188
+ logger.level = old_logger_level if logger
1189
+ end
1190
+
1191
+ # Overwrite the default class equality method to provide support for association proxies.
1192
+ def ===(object)
1193
+ object.is_a?(self)
1194
+ end
1195
+
1196
+ # Returns the base subclass that this class descends from. If A
1197
+ # extends CouchFoo::Base, A.base_class will return A. If B descends from A
1198
+ # through some arbitrarily deep hierarchy, B.base_class will return A.
1199
+ def base_class
1200
+ class_of_active_record_descendant(self)
1201
+ end
1202
+
1203
+ # Set this to true if this is an abstract class (see <tt>abstract_class?</tt>).
1204
+ attr_accessor :abstract_class
1205
+
1206
+ # Returns whether this class is a base CouchFoo class. If A is a base class and
1207
+ # B descends from A, then B.base_class will return B.
1208
+ def abstract_class?
1209
+ defined?(@abstract_class) && @abstract_class == true
1210
+ end
1211
+
1212
+ def respond_to?(method_id, include_private = false)
1213
+ if match = matches_dynamic_finder?(method_id) || matches_dynamic_finder_with_initialize_or_create?(method_id)
1214
+ return true if all_attributes_exists?(extract_attribute_names_from_match(match))
1215
+ end
1216
+ super
1217
+ end
1218
+
1219
+ # Returns a unique UUID even across multiple machines
1220
+ def get_uuid
1221
+ @uuid ||= UUID.new
1222
+ @uuid.generate
1223
+ end
1224
+
1225
+ private
1226
+ def find_initial(options)
1227
+ options.update(:limit => 1)
1228
+ find_every(options).first
1229
+ end
1230
+
1231
+ def find_last(options)
1232
+ options.update(:descending => true, :limit => 1)
1233
+ find_every(options).first
1234
+ end
1235
+
1236
+ def find_every(options)
1237
+ options = (scope(:find) || {}).merge(options)
1238
+ find_view(options)
1239
+ end
1240
+
1241
+ def find_from_ids(ids, options)
1242
+ expects_array = ids.first.kind_of?(Array)
1243
+ return ids.first if expects_array && ids.first.empty?
1244
+
1245
+ ids = ids.flatten.compact.uniq
1246
+
1247
+ case ids.size
1248
+ when 0
1249
+ raise DocumentNotFound, "Couldn't find #{name} without an ID"
1250
+ when 1
1251
+ result = find_one_by_id(ids.first, options)
1252
+ expects_array ? [ result ] : result
1253
+ else
1254
+ if (database.version > 0.8)
1255
+ conditions = options[:conditions] || {}
1256
+ find_view(conditions.merge(:keys => ids))
1257
+ else
1258
+ ids.map {|id| find_one_by_id(id, options) rescue nil}.compact
1259
+ end
1260
+ end
1261
+ end
1262
+
1263
+ # Find by document id. Only accepts the options :conditions and :readonly.
1264
+ def find_one_by_id(id, options)
1265
+ result = instantiate(database.get(id))
1266
+ # TODO This is bad, but more efficient in DB terms
1267
+ conditions = (scope(:find) || {}).merge(options[:conditions] || {})
1268
+ ({:ruby_class => document_class_name}.merge(conditions)).each do |key, value|
1269
+ raise DocumentNotFound unless result.read_attribute(key) == value
1270
+ end
1271
+ result.readonly! if options[:readonly]
1272
+ result
1273
+ end
1274
+
1275
+ # Finder methods must instantiate through this method to get the finder callbacks
1276
+ def instantiate(document)
1277
+ object =
1278
+ if subclass_name = document[inheritance_column]
1279
+ # No type given.
1280
+ if subclass_name.blank?
1281
+ allocate
1282
+ else
1283
+ begin
1284
+ compute_type(subclass_name).allocate
1285
+ rescue NameError
1286
+ raise SubclassNotFound,
1287
+ "The inheritance mechanism failed to locate the subclass: '#{document[inheritance_column]}'. " +
1288
+ "This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " +
1289
+ "Please rename this column if you didn't intend it to be used for storing the inheritance class " +
1290
+ "or overwrite #{self.to_s}.inheritance_column to use another column for that information."
1291
+ end
1292
+ end
1293
+ else
1294
+ allocate
1295
+ end
1296
+
1297
+ object.instance_variable_set("@attributes", check_document_types(document))
1298
+ object.instance_variable_set("@attributes_cache", Hash.new)
1299
+ object
1300
+ end
1301
+
1302
+ # Checks that the document only contains types that are listed as properties. Also converts
1303
+ # Date, DateTime and Time types into ruby objects (as we write them to the database in a JSON
1304
+ # format suitable for sorting)
1305
+ def check_document_types(record)
1306
+ property_types.each do |property, type|
1307
+ value = record[property.to_s]
1308
+ if (type == Date || type == DateTime || type == Time)
1309
+ record[property.to_s] = type.send(:parse, value) rescue nil
1310
+ else
1311
+ # Seems pointless but ensures we get attributes for properties that have been added to
1312
+ # model since document last saved
1313
+ record[property.to_s] = value
1314
+ end
1315
+ end
1316
+ record.reject!{|key, value| !(unchangeable_property_names + property_names).include?(key.to_sym)}
1317
+ record
1318
+ end
1319
+
1320
+ # Enables dynamic finders like find_by_user_name(user_name) and
1321
+ # find_by_user_name_and_password(user_name, password) that are turned into
1322
+ # find(:first, :conditions => ["user_name = ?", user_name]) and
1323
+ # find(:first, :conditions => ["user_name = ? AND password = ?", user_name, password])
1324
+ # respectively. Also works for find(:all) by using find_all_by_amount(50) that is turned into
1325
+ # find(:all, :conditions => ["amount = ?", 50]).
1326
+ #
1327
+ # It's even possible to use all the additional parameters to find. For example, the full interface
1328
+ # for find_all_by_amount is actually find_all_by_amount(amount, options).
1329
+ #
1330
+ # This also enables you to initialize a record if it is not found, such as
1331
+ # find_or_initialize_by_amount(amount) or find_or_create_by_user_and_password(user, password).
1332
+ #
1333
+ # Each dynamic finder or initializer/creator is also defined in the class after it is first invoked,
1334
+ # so that future attempts to use it do not run through method_missing.
1335
+ def method_missing(method_id, *arguments)
1336
+ if (view_names.include?(method_id))
1337
+ view = views.select{|v| v.name == method_id }.first
1338
+ generic_view(method_id.to_s, view.map_function, view.reduce_function, view.options.merge(arguments.first || {}))
1339
+ elsif match = matches_dynamic_finder?(method_id)
1340
+ finder = determine_finder(match)
1341
+
1342
+ attribute_names = extract_attribute_names_from_match(match)
1343
+ super unless all_attributes_exists?(attribute_names)
1344
+
1345
+ self.class_eval %{
1346
+ def self.#{method_id}(*args)
1347
+ options = args.extract_options!
1348
+ attributes = construct_attributes_from_arguments([:#{attribute_names.join(',:')}], args)
1349
+ finder_options = { :conditions => attributes }
1350
+ validate_find_options(options)
1351
+ set_readonly_option!(options)
1352
+
1353
+ ActiveSupport::Deprecation.silence { send(:#{finder}, options.merge(finder_options)) }
1354
+ end
1355
+ }, __FILE__, __LINE__
1356
+ send(method_id, *arguments)
1357
+ elsif match = matches_dynamic_finder_with_initialize_or_create?(method_id)
1358
+ instantiator = determine_instantiator(match)
1359
+ attribute_names = extract_attribute_names_from_match(match)
1360
+ super unless all_attributes_exists?(attribute_names)
1361
+
1362
+ self.class_eval %{
1363
+ def self.#{method_id}(*args)
1364
+ guard_protected_attributes = false
1365
+
1366
+ if args[0].is_a?(Hash)
1367
+ guard_protected_attributes = true
1368
+ attributes = args[0].with_indifferent_access
1369
+ find_attributes = attributes.slice(*[:#{attribute_names.join(',:')}])
1370
+ else
1371
+ find_attributes = attributes = construct_attributes_from_arguments([:#{attribute_names.join(',:')}], args)
1372
+ end
1373
+
1374
+ options = { :conditions => find_attributes }
1375
+
1376
+ record = find_initial(options)
1377
+
1378
+ if record.nil?
1379
+ record = self.new { |r| r.send(:attributes=, attributes, guard_protected_attributes) }
1380
+ record
1381
+ else
1382
+ record
1383
+ end
1384
+ end
1385
+ }, __FILE__, __LINE__
1386
+ send(method_id, *arguments)
1387
+ else
1388
+ super
1389
+ end
1390
+ end
1391
+
1392
+ def matches_dynamic_finder?(method_id)
1393
+ /^find_(all_by|by)_([_a-zA-Z]\w*)$/.match(method_id.to_s)
1394
+ end
1395
+
1396
+ def matches_dynamic_finder_with_initialize_or_create?(method_id)
1397
+ /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/.match(method_id.to_s)
1398
+ end
1399
+
1400
+ def determine_finder(match)
1401
+ match.captures.first == 'all_by' ? :find_every : :find_initial
1402
+ end
1403
+
1404
+ def determine_instantiator(match)
1405
+ match.captures.first == 'initialize' ? :new : :create
1406
+ end
1407
+
1408
+ def extract_attribute_names_from_match(match)
1409
+ match.captures.last.split('_and_')
1410
+ end
1411
+
1412
+ def construct_attributes_from_arguments(attribute_names, arguments)
1413
+ attributes = {}
1414
+ attribute_names.each_with_index { |name, idx| attributes[name] = arguments[idx] }
1415
+ attributes
1416
+ end
1417
+
1418
+ # Similar in purpose to +expand_hash_conditions_for_aggregates+.
1419
+ def expand_attribute_names_for_aggregates(attribute_names)
1420
+ expanded_attribute_names = []
1421
+ attribute_names.each do |attribute_name|
1422
+ unless (aggregation = reflect_on_aggregation(attribute_name.to_sym)).nil?
1423
+ aggregate_mapping(aggregation).each do |field_attr, aggregate_attr|
1424
+ expanded_attribute_names << field_attr
1425
+ end
1426
+ else
1427
+ expanded_attribute_names << attribute_name
1428
+ end
1429
+ end
1430
+ expanded_attribute_names
1431
+ end
1432
+
1433
+ def all_attributes_exists?(attribute_names)
1434
+ attribute_names = expand_attribute_names_for_aggregates(attribute_names)
1435
+ attribute_names.all? { |name| property_names.include?(name.to_sym) }
1436
+ end
1437
+
1438
+ # Defines an "attribute" method (like +inheritance_property+ or +document_name+). A new (class)
1439
+ # method will be created with the given name. If a value is specified, the new method will
1440
+ # return that value (as a string). Otherwise, the given block will be used to compute the
1441
+ # value of the method.
1442
+ #
1443
+ # The original method will be aliased, with the new name being prefixed with "original_".
1444
+ # This allows the new method to access the original value.
1445
+ #
1446
+ # Example:
1447
+ #
1448
+ # class A < CouchFoo::Base
1449
+ # define_attr_method :primary_key, "sysid"
1450
+ # define_attr_method( :inheritance_property ) do
1451
+ # original_inheritance_property + "_id"
1452
+ # end
1453
+ # end
1454
+ def define_attr_method(name, value=nil, &block)
1455
+ sing = class << self; self; end
1456
+ sing.send :alias_method, "original_#{name}", name
1457
+ if block_given?
1458
+ sing.send :define_method, name, &block
1459
+ else
1460
+ # use eval instead of a block to work around a memory leak in dev
1461
+ # mode in fcgi
1462
+ sing.class_eval "def #{name}; #{value.to_s.inspect}; end"
1463
+ end
1464
+ end
1465
+
1466
+ protected
1467
+ # Scope parameters to method calls within the block. Takes a hash of method_name => parameters hash.
1468
+ # method_name may be <tt>:find</tt> or <tt>:create</tt>. <tt>:find</tt> parameters may include the <tt>:conditions</tt>,
1469
+ # <tt>:limit</tt>, and <tt>:readonly</tt> options. <tt>:create</tt> parameters are an attributes hash.
1470
+ #
1471
+ # class Article < CouchFoo::Base
1472
+ # def self.create_with_scope
1473
+ # with_scope(:find => { :conditions => {:blog_id => 1} }, :create => { :blog_id => 1 }) do
1474
+ # find(1) # => SELECT * from articles WHERE blog_id = 1 AND id = 1
1475
+ # a = create(1)
1476
+ # a.blog_id # => 1
1477
+ # end
1478
+ # end
1479
+ # end
1480
+ #
1481
+ # In nested scopings, all previous parameters are overwritten by the innermost rule, with the exception of
1482
+ # <tt>:conditions</tt> option in <tt>:find</tt>, which is merged.
1483
+ #
1484
+ # class Article < CouchFoo::Base
1485
+ # def self.find_with_scope
1486
+ # with_scope(:find => { :conditions => {:blog_id => 1}, :limit => 1 }, :create => { :blog_id => 1 }) do
1487
+ # with_scope(:find => { :limit => 10 })
1488
+ # find(:all) # => SELECT * from articles WHERE blog_id = 1 LIMIT 10
1489
+ # end
1490
+ # with_scope(:find => { :conditions => "author_id = 3" })
1491
+ # find(:all) # => SELECT * from articles WHERE blog_id = 1 AND author_id = 3 LIMIT 1
1492
+ # end
1493
+ # end
1494
+ # end
1495
+ # end
1496
+ #
1497
+ # You can ignore any previous scopings by using the <tt>with_exclusive_scope</tt> method.
1498
+ #
1499
+ # class Article < CouchFoo::Base
1500
+ # def self.find_with_exclusive_scope
1501
+ # with_scope(:find => { :conditions => {:blog_id => 1}, :limit => 1 }) do
1502
+ # with_exclusive_scope(:find => { :limit => 10 })
1503
+ # find(:all) # => SELECT * from articles LIMIT 10
1504
+ # end
1505
+ # end
1506
+ # end
1507
+ # end
1508
+ def with_scope(method_scoping = {}, action = :merge, &block)
1509
+ method_scoping = method_scoping.method_scoping if method_scoping.respond_to?(:method_scoping)
1510
+
1511
+ # Dup first and second level of hash (method and params).
1512
+ method_scoping = method_scoping.inject({}) do |hash, (method, params)|
1513
+ hash[method] = (params == true) ? params : params.dup
1514
+ hash
1515
+ end
1516
+
1517
+ method_scoping.assert_valid_keys([ :find, :create ])
1518
+
1519
+ if f = method_scoping[:find]
1520
+ f.assert_valid_keys(VALID_FIND_OPTIONS)
1521
+ set_readonly_option! f
1522
+ end
1523
+
1524
+ # Merge scopings
1525
+ if action == :merge && current_scoped_methods
1526
+ method_scoping = current_scoped_methods.inject(method_scoping) do |hash, (method, params)|
1527
+ case hash[method]
1528
+ when Hash
1529
+ if method == :find
1530
+ (hash[method].keys + params.keys).uniq.each do |key|
1531
+ merge = hash[method][key] && params[key] # merge if both scopes have the same key
1532
+ if key == :conditions && merge
1533
+ hash[method][key] = params[key].merge(hash[method][key])
1534
+ else
1535
+ hash[method][key] = hash[method][key] || params[key]
1536
+ end
1537
+ end
1538
+ else
1539
+ hash[method] = params.merge(hash[method])
1540
+ end
1541
+ else
1542
+ hash[method] = params
1543
+ end
1544
+ hash
1545
+ end
1546
+ end
1547
+
1548
+ self.scoped_methods << method_scoping
1549
+
1550
+ begin
1551
+ yield
1552
+ ensure
1553
+ self.scoped_methods.pop
1554
+ end
1555
+ end
1556
+
1557
+ # Works like with_scope, but discards any nested properties.
1558
+ def with_exclusive_scope(method_scoping = {}, &block)
1559
+ with_scope(method_scoping, :overwrite, &block)
1560
+ end
1561
+
1562
+ # Test whether the given method and optional key are scoped.
1563
+ def scoped?(method, key = nil) #:nodoc:
1564
+ if current_scoped_methods && (scope = current_scoped_methods[method])
1565
+ !key || scope.has_key?(key)
1566
+ end
1567
+ end
1568
+
1569
+ # Retrieve the scope for the given method and optional key.
1570
+ def scope(method, key = nil) #:nodoc:
1571
+ if current_scoped_methods && (scope = current_scoped_methods[method])
1572
+ key ? scope[key] : scope
1573
+ end
1574
+ end
1575
+
1576
+ def scoped_methods #:nodoc:
1577
+ @scoped_methods ||= []
1578
+ end
1579
+
1580
+ def current_scoped_methods #:nodoc:
1581
+ scoped_methods.last
1582
+ end
1583
+
1584
+ # Returns the class type of the record using the current module as a prefix. So descendents of
1585
+ # MyApp::Business::Account would appear as MyApp::Business::AccountSubclass.
1586
+ def compute_type(type_name)
1587
+ modularized_name = (/^::/ =~ type_name) ? type_name : "#{parent.name}::#{type_name}"
1588
+ begin
1589
+ class_eval(modularized_name, __FILE__, __LINE__)
1590
+ rescue NameError
1591
+ class_eval(type_name, __FILE__, __LINE__)
1592
+ end
1593
+ end
1594
+
1595
+ # Returns the class descending directly from Active Record in the inheritance hierarchy.
1596
+ def class_of_active_record_descendant(klass)
1597
+ if klass.superclass == Base || klass.superclass.abstract_class?
1598
+ klass
1599
+ elsif klass.superclass.nil?
1600
+ raise ActiveRecordError, "#{name} doesn't belong in a hierarchy descending from ActiveRecord"
1601
+ else
1602
+ class_of_active_record_descendant(klass.superclass)
1603
+ end
1604
+ end
1605
+
1606
+ VALID_FIND_OPTIONS = [ :conditions, :include, :limit, :count, :order, :readonly, :offset, :use_key,
1607
+ :view_type, :startkey, :endkey, :return_json, :descending, :group, :group_level,
1608
+ :include_docs, :skip, :startkey_docid, :endkey_docid, :keys]
1609
+
1610
+ def validate_find_options(options) #:nodoc:
1611
+ options.assert_valid_keys(VALID_FIND_OPTIONS)
1612
+ end
1613
+
1614
+ def set_readonly_option!(options) #:nodoc:
1615
+ # Inherit :readonly from finder scope if set
1616
+ unless options.has_key?(:readonly)
1617
+ if scoped_readonly = scope(:find, :readonly)
1618
+ options[:readonly] = scoped_readonly
1619
+ end
1620
+ end
1621
+ end
1622
+ end # ClassMethods
1623
+
1624
+ public
1625
+ # New objects can be instantiated as either empty (pass no construction parameter) or pre-set with
1626
+ # attributes but not yet saved (pass a hash with key names matching the associated property names).
1627
+ # In both instances, valid attribute keys are determined by the property names of the model --
1628
+ # hence you can't have attributes that aren't part of the model.
1629
+ def initialize(attributes = nil)
1630
+ @attributes = attributes_from_property_definitions
1631
+ @attributes_cache = {}
1632
+ @new_record = true
1633
+ ensure_proper_type
1634
+ self.attributes = attributes
1635
+ self.class.send(:scope, :create).each { |att,value| self.send("#{att}=", value) } if self.class.send(:scoped?, :create)
1636
+ result = yield self if block_given?
1637
+ callback(:after_initialize) if respond_to_without_attributes?(:after_initialize)
1638
+ result
1639
+ end
1640
+
1641
+ # Returns the unqiue id of the document
1642
+ def _id
1643
+ attributes["_id"]
1644
+ end
1645
+ alias :id :_id
1646
+
1647
+ # Returns the revision id of the document
1648
+ def _rev
1649
+ attributes["_rev"]
1650
+ end
1651
+ alias :rev :_rev
1652
+
1653
+ # Returns the ruby_class of the document, as stored in the document to know which ruby object
1654
+ # to map back to
1655
+ def ruby_class
1656
+ attributes["ruby_class"]
1657
+ end
1658
+
1659
+ # Enables Couch Foo objects to be used as URL parameters in Action Pack automatically.
1660
+ def to_param
1661
+ (id = self.id) ? id.to_s : nil
1662
+ end
1663
+
1664
+ # Returns true if this object hasn't been saved yet -- that is, a record for the object doesn't exist yet.
1665
+ def new_record?
1666
+ defined?(@new_record) && @new_record
1667
+ end
1668
+
1669
+ # * No record exists: Creates a new record with values matching those of the object attributes.
1670
+ # * A record does exist: Updates the record with values matching those of the object attributes.
1671
+ #
1672
+ # Note: If your model specifies any validations then the method declaration dynamically
1673
+ # changes to:
1674
+ # save(perform_validation=true, bulk_save = self.class.database.bulk_save?)
1675
+ # Calling save(false) saves the model without running validations.
1676
+ # See CouchFoo::Validations for more information.
1677
+ def save
1678
+ create_or_update
1679
+ end
1680
+
1681
+ # Attempts to save the record, but instead of just returning false if it couldn't happen, it
1682
+ # raises a DocumentNotSaved exception.
1683
+ def save!
1684
+ create_or_update || raise(DocumentNotSaved)
1685
+ end
1686
+
1687
+ def destroy
1688
+ unless new_record?
1689
+ self.class.database.delete(@attributes)
1690
+ end
1691
+ freeze
1692
+ end
1693
+
1694
+ def clone
1695
+ attrs = clone_attributes(:read_attribute_before_type_cast)
1696
+ attributes_protected_by_default.each {|a| attrs.delete(a)}
1697
+ record = self.class.new
1698
+ record.attributes = attrs, false
1699
+ record
1700
+ end
1701
+
1702
+ # Returns an instance of the specified +klass+ with the attributes of the current record. This
1703
+ # is mostly useful in relation to inheritance structures where you want a subclass to appear as
1704
+ # the superclass. This can be used along with record identification in Action Pack
1705
+ # to allow, say, <tt>Client < Company</tt> to do something like render
1706
+ # <tt>:partial => @client.becomes(Company)</tt> to render that instance using the
1707
+ # companies/company partial instead of clients/client.
1708
+ #
1709
+ # Note: The new instance will share a link to the same attributes as the original class. So any
1710
+ # change to the attributes in either instance will affect the other.
1711
+ def becomes(klass)
1712
+ returning klass.new do |became|
1713
+ became.instance_variable_set("@attributes", @attributes)
1714
+ became.instance_variable_set("@attributes_cache", @attributes_cache)
1715
+ became.instance_variable_set("@new_record", new_record?)
1716
+ end
1717
+ end
1718
+
1719
+ # Updates a single attribute and saves the record. This is especially useful for boolean flags
1720
+ # on existing records.
1721
+ # Note: This method is overwritten by the Validation module that'll make sure that updates made
1722
+ # with this method aren't subjected to validation checks. Hence, attributes can be updated even
1723
+ # if the full object isn't valid.
1724
+ def update_attribute(name, value)
1725
+ send(name.to_s + '=', value)
1726
+ save
1727
+ end
1728
+
1729
+ # Updates all the attributes from the passed-in Hash and saves the record. If the object is
1730
+ # invalid, the saving will fail and false will be returned.
1731
+ def update_attributes(attributes)
1732
+ self.attributes = attributes
1733
+ save
1734
+ end
1735
+
1736
+ # Updates an object just like Base.update_attributes but calls save! instead of save so an
1737
+ # exception is raised if the record is invalid.
1738
+ def update_attributes!(attributes)
1739
+ self.attributes = attributes
1740
+ save!
1741
+ end
1742
+
1743
+ # Initializes +attribute+ to zero if +nil+ and adds the value passed as +by+ (default is 1).
1744
+ # The increment is performed directly on the underlying attribute, no setter is invoked.
1745
+ # Only makes sense for number-based attributes. Returns +self+.
1746
+ def increment(attribute, by = 1)
1747
+ self[attribute] ||= 0
1748
+ self[attribute] += by
1749
+ self
1750
+ end
1751
+
1752
+ # Wrapper around +increment+ that saves the record. This method differs from
1753
+ # its non-bang version in that it passes through the attribute setter.
1754
+ # Saving is not subjected to validation checks. Returns +true+ if the
1755
+ # record could be saved.
1756
+ def increment!(attribute, by = 1)
1757
+ increment(attribute, by).update_attribute(attribute, self[attribute])
1758
+ end
1759
+
1760
+ # Initializes +attribute+ to zero if +nil+ and subtracts the value passed as +by+ (default is 1).
1761
+ # The decrement is performed directly on the underlying attribute, no setter is invoked.
1762
+ # Only makes sense for number-based attributes. Returns +self+.
1763
+ def decrement(attribute, by = 1)
1764
+ self[attribute] ||= 0
1765
+ self[attribute] -= by
1766
+ self
1767
+ end
1768
+
1769
+ # Wrapper around +decrement+ that saves the record. This method differs from
1770
+ # its non-bang version in that it passes through the attribute setter.
1771
+ # Saving is not subjected to validation checks. Returns +true+ if the
1772
+ # record could be saved.
1773
+ def decrement!(attribute, by = 1)
1774
+ decrement(attribute, by).update_attribute(attribute, self[attribute])
1775
+ end
1776
+
1777
+ # Assigns to +attribute+ the boolean opposite of <tt>attribute?</tt>. So
1778
+ # if the predicate returns +true+ the attribute will become +false+. This
1779
+ # method toggles directly the underlying value without calling any setter.
1780
+ # Returns +self+.
1781
+ def toggle(attribute)
1782
+ self[attribute] = !send("#{attribute}?")
1783
+ self
1784
+ end
1785
+
1786
+ # Wrapper around +toggle+ that saves the record. This method differs from
1787
+ # its non-bang version in that it passes through the attribute setter.
1788
+ # Saving is not subjected to validation checks. Returns +true+ if the
1789
+ # record could be saved.
1790
+ def toggle!(attribute)
1791
+ toggle(attribute).update_attribute(attribute, self[attribute])
1792
+ end
1793
+
1794
+ # Reloads the attributes of this object from the database. The optional options argument is
1795
+ # passed to find when reloading so you may do e.g. record.reload(:lock => true) to reload the
1796
+ # same record with an exclusive row lock.
1797
+ def reload(options = nil)
1798
+ #clear_aggregation_cache
1799
+ clear_association_cache
1800
+ @attributes.update(self.class.find(self.id, options).instance_variable_get('@attributes'))
1801
+ @attributes_cache = {}
1802
+ self
1803
+ end
1804
+
1805
+ # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast
1806
+ # (for example, "2004-12-12" in a data property is cast to a date object,
1807
+ # like Date.new(2004, 12, 12)).
1808
+ # (Alias for the protected read_attribute method).
1809
+ def [](attr_name)
1810
+ read_attribute(attr_name)
1811
+ end
1812
+
1813
+ # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+.
1814
+ # (Alias for the protected write_attribute method).
1815
+ def []=(attr_name, value)
1816
+ write_attribute(attr_name, value)
1817
+ end
1818
+
1819
+ # Allows you to set all the attributes at once by passing in a hash with keys matching the
1820
+ # attribute names (which again matches the property names). Sensitive attributes can be protected
1821
+ # from this form of mass-assignment by using the +attr_protected+ macro. Or you can alternatively
1822
+ # specify which attributes *can* be accessed with the +attr_accessible+ macro. Then all the
1823
+ # attributes not included in that won't be allowed to be mass-assigned.
1824
+ def attributes=(new_attributes, guard_protected_attributes = true)
1825
+ return if new_attributes.nil?
1826
+ attributes = new_attributes.dup
1827
+ attributes.stringify_keys!
1828
+
1829
+ multi_parameter_attributes = []
1830
+ attributes = remove_attributes_protected_from_mass_assignment(attributes) if guard_protected_attributes
1831
+
1832
+ attributes.each do |k, v|
1833
+ k.include?("(") ? multi_parameter_attributes << [ k, v ] : send(k + "=", v)
1834
+ end
1835
+
1836
+ assign_multiparameter_attributes(multi_parameter_attributes)
1837
+ end
1838
+
1839
+ # Returns a hash of all the attributes with their names as keys and the values of the
1840
+ # attributes as values.
1841
+ def attributes
1842
+ self.attribute_names.inject({}) do |attrs, name|
1843
+ attrs[name] = read_attribute(name)
1844
+ attrs
1845
+ end
1846
+ end
1847
+
1848
+ # Returns a hash of attributes before typecasting and deserialization.
1849
+ def attributes_before_type_cast
1850
+ self.attribute_names.inject({}) do |attrs, name|
1851
+ attrs[name] = read_attribute_before_type_cast(name)
1852
+ attrs
1853
+ end
1854
+ end
1855
+
1856
+ # Format attributes nicely for inspect.
1857
+ def attribute_for_inspect(attr_name)
1858
+ value = read_attribute(attr_name)
1859
+
1860
+ if value.is_a?(String) && value.length > 50
1861
+ "#{value[0..50]}...".inspect
1862
+ elsif value.is_a?(Date) || value.is_a?(Time)
1863
+ %("#{value.to_s(:db)}")
1864
+ else
1865
+ value.inspect
1866
+ end
1867
+ end
1868
+
1869
+ # Returns true if the specified +attribute+ has been set by the user or by a database load and is neither
1870
+ # nil nor empty? (the latter only applies to objects that respond to empty?, most notably Strings).
1871
+ def attribute_present?(attribute)
1872
+ value = read_attribute(attribute)
1873
+ !value.blank?
1874
+ end
1875
+
1876
+ # Returns true if the given attribute is in the attributes hash
1877
+ def has_attribute?(attr_name)
1878
+ @attributes.has_key?(attr_name.to_s)
1879
+ end
1880
+
1881
+ # Returns an array of names for the attributes available on this object sorted alphabetically.
1882
+ def attribute_names
1883
+ @attributes.keys.map{|a| a.to_s}.sort
1884
+ end
1885
+
1886
+ # Returns true if the +comparison_object+ is the same object, or is of the same type and has the same id.
1887
+ def ==(comparison_object)
1888
+ comparison_object.equal?(self) ||
1889
+ (comparison_object.instance_of?(self.class) &&
1890
+ comparison_object.id == id &&
1891
+ !comparison_object.new_record?)
1892
+ end
1893
+
1894
+ # Delegates to ==
1895
+ def eql?(comparison_object)
1896
+ self == (comparison_object)
1897
+ end
1898
+
1899
+ # Delegates to id in order to allow two records of the same type and id to work with something like:
1900
+ # [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ]
1901
+ def hash
1902
+ id.hash
1903
+ end
1904
+
1905
+ # Freeze the attributes hash such that associations are still accessible, even on destroyed records.
1906
+ def freeze
1907
+ @attributes.freeze; self
1908
+ end
1909
+
1910
+ # Returns +true+ if the attributes hash has been frozen.
1911
+ def frozen?
1912
+ @attributes.frozen?
1913
+ end
1914
+
1915
+ # Returns +true+ if the record is read only. Records loaded through joins with piggy-back
1916
+ # attributes will be marked as read only since they cannot be saved.
1917
+ def readonly?
1918
+ defined?(@readonly) && @readonly == true
1919
+ end
1920
+
1921
+ # Marks this record as read only.
1922
+ def readonly!
1923
+ @readonly = true
1924
+ end
1925
+
1926
+ # Returns the contents of the record as a nicely formatted string.
1927
+ def inspect
1928
+ attributes_as_nice_string = (self.class.property_names + unchangeable_property_names).collect { |name|
1929
+ "#{name}: #{attribute_for_inspect(name)}"
1930
+ }.compact.join(", ")
1931
+ "#<#{self.class} #{attributes_as_nice_string}>"
1932
+ end
1933
+
1934
+ private
1935
+ def create_or_update
1936
+ raise ReadOnlyRecord if readonly?
1937
+ result = new_record? ? create : update
1938
+ result != false
1939
+ end
1940
+
1941
+ def update
1942
+ begin
1943
+ response = self.class.database.save(attributes_for_save)
1944
+ @attributes["_rev"] = response['rev']
1945
+ 1
1946
+ rescue Exception => e
1947
+ logger.error "Unable to update document: #{e.message}"
1948
+ false
1949
+ end
1950
+ end
1951
+
1952
+ def create
1953
+ @attributes["_id"] = self.class.get_uuid
1954
+ begin
1955
+ response = self.class.database.save(attributes_for_save.reject{|key,value| key == "_rev"})
1956
+ @attributes["_rev"] = response['rev']
1957
+ @new_record = false
1958
+ @attributes["_id"]
1959
+ rescue Exception => e
1960
+ @attributes["_id"] = nil
1961
+ logger.error "Unable to create document: #{e.message}"
1962
+ false
1963
+ end
1964
+ end
1965
+
1966
+ # Attributes but with date/time types convert to JSON sortable format. This creates a copy of
1967
+ # the original attributes for saving so doesn't alter the attributes accessible to the user
1968
+ def attributes_for_save
1969
+ attrs = attributes
1970
+ self.class.property_types.each do |name, type|
1971
+ if (type == Date || type == DateTime || type == Time)
1972
+ attrs[name.to_s] = attrs[name.to_s].strftime("%Y/%m/%d %H:%M:%S +0000") if attrs[name.to_s]
1973
+ end
1974
+ end
1975
+ attrs
1976
+ end
1977
+
1978
+ # Sets the attribute used for inheritance to this class name if this is not the CouchFoo::Base
1979
+ # descendent. Considering the hierarchy Reply < Message < ActiveRecord::Base, this makes it
1980
+ # possible to do Reply.new without having to set <tt>Reply[Reply.inheritance_column] = "Reply"</tt>
1981
+ # yourself. No such attribute would be set for objects of the Message class in that example.
1982
+ def ensure_proper_type
1983
+ unless self.class.descends_from_couch_foo?
1984
+ write_attribute(self.class.inheritance_column, self.class.name)
1985
+ end
1986
+ end
1987
+
1988
+ def remove_attributes_protected_from_mass_assignment(attributes)
1989
+ safe_attributes =
1990
+ if self.class.accessible_attributes.nil? && self.class.protected_attributes.nil?
1991
+ attributes.reject { |key, value| attributes_protected_by_default.include?(key.gsub(/\(.+/, "")) }
1992
+ elsif self.class.protected_attributes.nil?
1993
+ attributes.reject { |key, value| !self.class.accessible_attributes.include?(key.gsub(/\(.+/, "")) || attributes_protected_by_default.include?(key.gsub(/\(.+/, "")) }
1994
+ elsif self.class.accessible_attributes.nil?
1995
+ attributes.reject { |key, value| self.class.protected_attributes.include?(key.gsub(/\(.+/,"")) || attributes_protected_by_default.include?(key.gsub(/\(.+/, "")) }
1996
+ else
1997
+ raise "Declare either attr_protected or attr_accessible for #{self.class}, but not both."
1998
+ end
1999
+
2000
+ removed_attributes = attributes.keys - safe_attributes.keys
2001
+
2002
+ if removed_attributes.any?
2003
+ logger.debug "WARNING: Can't mass-assign these protected attributes: #{removed_attributes.join(', ')}"
2004
+ end
2005
+
2006
+ safe_attributes
2007
+ end
2008
+
2009
+ def attributes_protected_by_default
2010
+ attributes = @@unchangeable_property_names + [self.class.inheritance_column]
2011
+ attributes.map{|p| p.to_s}
2012
+ end
2013
+
2014
+ def attributes_from_property_definitions
2015
+ attribs = {}
2016
+ attribs["_id"] = nil
2017
+ attribs["_rev"] = nil
2018
+ attribs["ruby_class"] = self.class.document_class_name
2019
+ self.class.properties.inject(attribs) do |attributes, property|
2020
+ attributes[property.name.to_s] = property.default
2021
+ attributes
2022
+ end
2023
+ end
2024
+
2025
+ # Instantiates objects for all attribute classes that needs more than one constructor parameter.
2026
+ # This is done by calling new on the property type or aggregation type (through composed_of)
2027
+ # object with these parameters. So having the pairs written_on(1) = "2004",
2028
+ # written_on(2) = "6", written_on(3) = "24", will instantiate written_on (a date type) with
2029
+ # Date.new("2004", "6", "24"). You can also specify a typecast character in the parentheses to
2030
+ # have the parameters typecasted before they're used in the constructor. Use i for Fixnum,
2031
+ # f for Float, s for String, and a for Array. If all the values for a given attribute are empty,
2032
+ # the attribute will be set to nil.
2033
+ def assign_multiparameter_attributes(pairs)
2034
+ execute_callstack_for_multiparameter_attributes(
2035
+ extract_callstack_for_multiparameter_attributes(pairs)
2036
+ )
2037
+ end
2038
+
2039
+ def instantiate_time_object(name, values)
2040
+ Time.time_with_datetime_fallback(@@default_timezone, *values)
2041
+ end
2042
+
2043
+ def execute_callstack_for_multiparameter_attributes(callstack)
2044
+ errors = []
2045
+ callstack.each do |name, values|
2046
+ klass = type_for_property(name)
2047
+ if values.empty?
2048
+ send(name + "=", nil)
2049
+ else
2050
+ begin
2051
+ value = if klass == Time
2052
+ instantiate_time_object(name, values)
2053
+ elsif klass == Date
2054
+ begin
2055
+ Date.new(*values)
2056
+ rescue ArgumentError => ex # if Date.new raises an exception on an invalid date
2057
+ instantiate_time_object(name, values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates
2058
+ end
2059
+ else
2060
+ klass.new(*values)
2061
+ end
2062
+
2063
+ send(name + "=", value)
2064
+ rescue => ex
2065
+ errors << AttributeAssignmentError.new("error on assignment #{values.inspect} to #{name}", ex, name)
2066
+ end
2067
+ end
2068
+ end
2069
+ unless errors.empty?
2070
+ raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes"
2071
+ end
2072
+ end
2073
+
2074
+ def extract_callstack_for_multiparameter_attributes(pairs)
2075
+ attributes = { }
2076
+
2077
+ for pair in pairs
2078
+ multiparameter_name, value = pair
2079
+ attribute_name = multiparameter_name.split("(").first
2080
+ attributes[attribute_name] = [] unless attributes.include?(attribute_name)
2081
+
2082
+ unless value.empty?
2083
+ attributes[attribute_name] <<
2084
+ [ find_parameter_position(multiparameter_name), type_cast_attribute_value(multiparameter_name, value) ]
2085
+ end
2086
+ end
2087
+
2088
+ attributes.each { |name, values| attributes[name] = values.sort_by{ |v| v.first }.collect { |v| v.last } }
2089
+ end
2090
+
2091
+ def type_cast_attribute_value(multiparameter_name, value)
2092
+ multiparameter_name =~ /\([0-9]*([a-z])\)/ ? value.send("to_" + $1) : value
2093
+ end
2094
+
2095
+ def find_parameter_position(multiparameter_name)
2096
+ multiparameter_name.scan(/\(([0-9]*).*\)/).first.first
2097
+ end
2098
+
2099
+ def clone_attributes(reader_method = :read_attribute, attributes = {})
2100
+ self.attribute_names.inject(attributes) do |attrs, name|
2101
+ attrs[name] = clone_attribute_value(reader_method, name)
2102
+ attrs
2103
+ end
2104
+ end
2105
+
2106
+ def clone_attribute_value(reader_method, attribute_name)
2107
+ value = send(reader_method, attribute_name)
2108
+ value.duplicable? ? value.clone : value
2109
+ rescue TypeError, NoMethodError
2110
+ value
2111
+ end
2112
+
2113
+ def type_for_property(name)
2114
+ self.class.property_types[name.to_sym]
2115
+ end
2116
+ end
2117
+ end