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,117 @@
1
+ module CouchFoo
2
+ module Calculations #:nodoc:
3
+ CALCULATIONS_OPTIONS = [:conditions, :order, :distinct, :limit, :count, :offset, :include]
4
+ def self.included(base)
5
+ base.extend(ClassMethods)
6
+ end
7
+
8
+ module ClassMethods
9
+ # Count operates using two different approaches.
10
+ #
11
+ # * Count all: By not passing any parameters to count, it will return a count of all the rows for the model.
12
+ # * Count using options will find the row count matched by the options used.
13
+ #
14
+ # The second approach, count using options, accepts an option hash as the only parameter. The options are:
15
+ #
16
+ # * <tt>:conditions</tt>: A conditions hash like { :user_name => username }. See conditions in the intro.
17
+ # * <tt>:include</tt>: Named associations that should be loaded at the same time. See eager loading
18
+ # under Associations.
19
+ # * <tt>:order</tt>: An element to order on, eg :order => :user_name
20
+ # * <tt>:distinct</tt>: Set this to true to remove repeating elements
21
+ #
22
+ # Examples for counting all:
23
+ # Person.count # returns the total count of all people
24
+ #
25
+ # Examples for count with options:
26
+ # Person.count(:conditions => {age = 26})
27
+ #
28
+ # Note: <tt>Person.count(:all)</tt> will not work because it will use <tt>:all</tt> as the condition. Use Person.count instead.
29
+ def count(options = {})
30
+ calculate(:count, nil, options)
31
+ end
32
+
33
+ # Calculates the average value on a given property. The value is returned as a float. See +calculate+ for examples with options.
34
+ #
35
+ # Person.average('age')
36
+ def average(attribute_name, options = {})
37
+ calculate(:avg, attribute_name, options)
38
+ end
39
+
40
+ # Calculates the minimum value on a given property. The value is returned with the same data type of the property. See +calculate+ for examples with options.
41
+ #
42
+ # Person.minimum('age')
43
+ def minimum(attribute_name, options = {})
44
+ calculate(:min, attribute_name, options)
45
+ end
46
+
47
+ # Calculates the maximum value on a given property. The value is returned with the same data type of the property. See +calculate+ for examples with options.
48
+ #
49
+ # Person.maximum('age')
50
+ def maximum(attribute_name, options = {})
51
+ calculate(:max, attribute_name, options)
52
+ end
53
+
54
+ # Calculates the sum of values on a given property. The value is returned with the same data type of the property. See +calculate+ for examples with options.
55
+ #
56
+ # Person.sum('age')
57
+ def sum(attribute_name, options = {})
58
+ calculate(:sum, attribute_name, options)
59
+ end
60
+
61
+ # This calculates aggregate values in the given property. Methods for count, sum, average, minimum,
62
+ # and maximum have been added as shortcuts.
63
+ # Options such as <tt>:conditions</tt>, <tt>:order</tt>, <tt>:count</tt> and <tt>:distinct</tt>
64
+ # can be passed to customize the query.
65
+ #
66
+ # Options:
67
+ # * <tt>:conditions</tt>: A conditions hash like { :user_name => username }. See conditions in the intro.
68
+ # * <tt>:include</tt>: Named associations that should be loaded at the same time. See eager loading
69
+ # under Associations.
70
+ # * <tt>:order</tt>: An element to order on, eg :order => :user_name
71
+ # * <tt>:distinct</tt>: Set this to true to remove repeating elements
72
+ #
73
+ # Examples:
74
+ # Person.calculate(:count, :all) # The same as Person.count
75
+ # Person.average(:age) # Find the average of people
76
+ # Person.minimum(:age, :conditions => {:last_name => 'Drake'}) # Finds the minimum age for everyone with a last name other than 'Drake'
77
+ # Person.sum("2 * age")
78
+ def calculate(operation, attribute_name, options = {})
79
+ validate_calculation_options(operation, options)
80
+ catch :invalid_query do
81
+ return execute_simple_calculation(operation, attribute_name, options)
82
+ end
83
+ 0
84
+ end
85
+
86
+ protected
87
+ def execute_simple_calculation(operation, attribute_name, options) #:nodoc:
88
+ if operation == :count
89
+ value = count_view(options)
90
+ else
91
+ raise "NotImplementedYet"
92
+ #TODO test sum on associationproxy when done
93
+ end
94
+ type_cast_calculated_value(value, attribute_name, operation)
95
+ end
96
+
97
+ private
98
+ def validate_calculation_options(operation, options = {})
99
+ options.assert_valid_keys(CALCULATIONS_OPTIONS)
100
+ end
101
+
102
+ def type_cast_calculated_value(value, attribute_name, operation = nil)
103
+ operation = operation.to_s.downcase
104
+ case operation
105
+ when 'count' then value.to_i
106
+ when 'sum' then type_cast_using_property(value || '0', attribute_name)
107
+ when 'avg' then value && value.to_d
108
+ else type_cast_using_property(value, attribute_name)
109
+ end
110
+ end
111
+
112
+ def type_cast_using_property(value, attribute_name)
113
+ attribute_name ? convert_to_type(value, type_for_property(attribute_name.to_sym)) : value
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,311 @@
1
+ require 'observer'
2
+
3
+ module CouchFoo
4
+ # Callbacks are hooks into the lifecycle of a Couch Foo object that allow you to trigger logic
5
+ # before or after an alteration of the object state. This can be used to make sure that associated
6
+ # and dependent objects are deleted when destroy is called (by overwriting +before_destroy+) or to
7
+ # massage attributes before they're validated (by overwriting +before_validation+). As an example
8
+ # of the callbacks initiated, consider the <tt>Base#save</tt> call:
9
+ #
10
+ # * (-) <tt>save</tt>
11
+ # * (-) <tt>valid</tt>
12
+ # * (1) <tt>before_validation</tt>
13
+ # * (2) <tt>before_validation_on_create</tt>
14
+ # * (-) <tt>validate</tt>
15
+ # * (-) <tt>validate_on_create</tt>
16
+ # * (3) <tt>after_validation</tt>
17
+ # * (4) <tt>after_validation_on_create</tt>
18
+ # * (5) <tt>before_save</tt>
19
+ # * (6) <tt>before_create</tt>
20
+ # * (-) <tt>create</tt>
21
+ # * (7) <tt>after_create</tt>
22
+ # * (8) <tt>after_save</tt>
23
+ #
24
+ # That's a total of eight callbacks, which gives you immense power to react and prepare for each state in the
25
+ # Couch Foo lifecycle.
26
+ #
27
+ # Examples:
28
+ # class CreditCard < CouchFoo::Base
29
+ # # Strip everything but digits, so the user can specify "555 234 34" or
30
+ # # "5552-3434" or both will mean "55523434"
31
+ # def before_validation_on_create
32
+ # self.number = number.gsub(/[^0-9]/, "") if attribute_present?("number")
33
+ # end
34
+ # end
35
+ #
36
+ # class Subscription < CouchFoo::Base
37
+ # before_create :record_signup
38
+ #
39
+ # private
40
+ # def record_signup
41
+ # self.signed_up_on = Date.today
42
+ # end
43
+ # end
44
+ #
45
+ # class Firm < CouchFoo::Base
46
+ # # Destroys the associated clients and people when the firm is destroyed
47
+ # before_destroy { |record| Person.destroy_all "firm_id = #{record.id}" }
48
+ # before_destroy { |record| Client.destroy_all "client_of = #{record.id}" }
49
+ # end
50
+ #
51
+ # == Inheritable callback queues
52
+ #
53
+ # Besides the overwriteable callback methods, it's also possible to register callbacks through the use
54
+ # of the callback macros. Their main advantage is that the macros add behavior into a callback queue
55
+ # that is kept intact down through an inheritance hierarchy. Example:
56
+ #
57
+ # class Topic < CouchFoo::Base
58
+ # before_destroy :destroy_author
59
+ # end
60
+ #
61
+ # class Reply < Topic
62
+ # before_destroy :destroy_readers
63
+ # end
64
+ #
65
+ # Now, when <tt>Topic#destroy</tt> is run only +destroy_author+ is called. When <tt>Reply#destroy</tt>
66
+ # is run, both +destroy_author+ and +destroy_readers+ are called. Contrast this to the situation where
67
+ # we've implemented the save behavior through overwriteable methods:
68
+ #
69
+ # class Topic < CouchFoo::Base
70
+ # def before_destroy() destroy_author end
71
+ # end
72
+ #
73
+ # class Reply < Topic
74
+ # def before_destroy() destroy_readers end
75
+ # end
76
+ #
77
+ # In that case, <tt>Reply#destroy</tt> would only run +destroy_readers+ and _not_ +destroy_author+.
78
+ # So, use the callback macros when you want to ensure that a certain callback is called for the
79
+ # entire hierarchy, and use the regular overwriteable methods when you want to leave it up to each
80
+ # descendent to decide whether they want to call +super+ and trigger the inherited callbacks.
81
+ #
82
+ # *IMPORTANT:* In order for inheritance to work for the callback queues, you must specify the callbacks
83
+ # before specifying the associations. Otherwise, you might trigger the loading of a child before the
84
+ # parent has registered the callbacks and they won't be inherited.
85
+ #
86
+ # == Types of callbacks
87
+ #
88
+ # There are four types of callbacks accepted by the callback macros: Method references (symbol),
89
+ # callback objects, inline methods (using a proc), and inline eval methods (using a string). Method
90
+ # references and callback objects are the recommended approaches, inline methods using a proc are
91
+ # sometimes appropriate (such as for creating mix-ins), and inline eval methods are deprecated.
92
+ #
93
+ # The method reference callbacks work by specifying a protected or private method available in the
94
+ # object, like this:
95
+ #
96
+ # class Topic < CouchFoo::Base
97
+ # before_destroy :delete_parents
98
+ #
99
+ # private
100
+ # def delete_parents
101
+ # self.class.delete_all "parent_id = #{id}"
102
+ # end
103
+ # end
104
+ #
105
+ # The callback objects have methods named after the callback called with the record as the only
106
+ # parameter, such as:
107
+ #
108
+ # class BankAccount < CouchFoo::Base
109
+ # before_save EncryptionWrapper.new("credit_card_number")
110
+ # after_save EncryptionWrapper.new("credit_card_number")
111
+ # after_initialize EncryptionWrapper.new("credit_card_number")
112
+ # end
113
+ #
114
+ # class EncryptionWrapper
115
+ # def initialize(attribute)
116
+ # @attribute = attribute
117
+ # end
118
+ #
119
+ # def before_save(record)
120
+ # record.credit_card_number = encrypt(record.credit_card_number)
121
+ # end
122
+ #
123
+ # def after_save(record)
124
+ # record.credit_card_number = decrypt(record.credit_card_number)
125
+ # end
126
+ #
127
+ # alias_method :after_find, :after_save
128
+ #
129
+ # private
130
+ # def encrypt(value)
131
+ # # Secrecy is committed
132
+ # end
133
+ #
134
+ # def decrypt(value)
135
+ # # Secrecy is unveiled
136
+ # end
137
+ # end
138
+ #
139
+ # So you specify the object you want messaged on a given callback. When that callback is triggered,
140
+ # the object has a method by the name of the callback messaged.
141
+ #
142
+ # The callback macros usually accept a symbol for the method they're supposed to run, but you can
143
+ # also pass a "method string", which will then be evaluated within the binding of the callback. Example:
144
+ #
145
+ # class Topic < CouchFoo::Base
146
+ # before_destroy 'self.class.delete_all ":parent_id => #{id}"'
147
+ # end
148
+ #
149
+ # Notice that single quotes (') are used so the <tt>#{id}</tt> part isn't evaluated until the callback
150
+ # is triggered. Also note that these inline callbacks can be stacked just like the regular ones:
151
+ #
152
+ # class Topic < CouchFoo::Base
153
+ # before_destroy 'self.class.delete_all ":parent_id => #{id}"',
154
+ # 'puts "Evaluated after parents are destroyed"'
155
+ # end
156
+ #
157
+ # == The +after_find+ and +after_initialize+ exceptions
158
+ #
159
+ # Because +after_find+ and +after_initialize+ are called for each object found and instantiated by
160
+ # a finder, such as <tt>Base.find(:all)</tt>, we've had to implement a simple performance constraint
161
+ # (50% more speed on a simple test case). Unlike all the other callbacks, +after_find+ and
162
+ # +after_initialize+ will only be run if an explicit implementation is defined (<tt>def after_find</tt>).
163
+ # In that case, all of the callback types will be called.
164
+ #
165
+ # == <tt>before_validation*</tt> returning statements
166
+ #
167
+ # If the returning value of a +before_validation+ callback can be evaluated to +false+, the process will
168
+ # be aborted and <tt>Base#save</tt> will return +false+. If Base#save! is called it will raise a
169
+ # RecordNotSaved exception. Nothing will be appended to the errors object.
170
+ #
171
+ # == Canceling callbacks
172
+ #
173
+ # If a <tt>before_*</tt> callback returns +false+, all the later callbacks and the associated action
174
+ # are cancelled. If an <tt>after_*</tt> callback returns +false+, all the later callbacks are cancelled.
175
+ # Callbacks are generally run in the order they are defined, with the exception of callbacks defined as
176
+ # methods on the model, which are called last.
177
+ module Callbacks
178
+ CALLBACKS = %w(
179
+ after_find after_initialize before_save after_save before_create after_create before_update after_update
180
+ before_validation after_validation before_validation_on_create after_validation_on_create
181
+ before_validation_on_update after_validation_on_update before_destroy after_destroy
182
+ )
183
+
184
+ def self.included(base) #:nodoc:
185
+ base.extend Observable
186
+
187
+ [:create_or_update, :valid?, :create, :update, :destroy].each do |method|
188
+ base.send :alias_method_chain, method, :callbacks
189
+ end
190
+
191
+ base.send :include, ActiveSupport::Callbacks
192
+ base.define_callbacks *CALLBACKS
193
+ end
194
+
195
+ # Is called _before_ <tt>Base.save</tt> (regardless of whether it's a +create+ or +update+ save).
196
+ def before_save() end
197
+
198
+ # Is called _after_ <tt>Base.save</tt> (regardless of whether it's a +create+ or +update+ save).
199
+ #
200
+ # class Contact < CouchFoo::Base
201
+ # after_save { logger.info( 'New contact saved!' ) }
202
+ # end
203
+ def after_save() end
204
+ def create_or_update_with_callbacks() #:nodoc:
205
+ return false if callback(:before_save) == false
206
+ result = create_or_update_without_callbacks()
207
+ callback(:after_save)
208
+ result
209
+ end
210
+ private :create_or_update_with_callbacks
211
+
212
+ # Is called _before_ <tt>Base.save</tt> on new objects that haven't been saved yet (no record exists).
213
+ def before_create() end
214
+
215
+ # Is called _after_ <tt>Base.save</tt> on new objects that haven't been saved yet (no record exists).
216
+ def after_create() end
217
+ def create_with_callbacks() #:nodoc:
218
+ return false if callback(:before_create) == false
219
+ result = create_without_callbacks()
220
+ callback(:after_create)
221
+ result
222
+ end
223
+ private :create_with_callbacks
224
+
225
+ # Is called _before_ <tt>Base.save</tt> on existing objects that have a record.
226
+ def before_update() end
227
+
228
+ # Is called _after_ <tt>Base.save</tt> on existing objects that have a record.
229
+ def after_update() end
230
+
231
+ def update_with_callbacks(*args) #:nodoc:
232
+ return false if callback(:before_update) == false
233
+ result = update_without_callbacks(*args)
234
+ callback(:after_update)
235
+ result
236
+ end
237
+ private :update_with_callbacks
238
+
239
+ # Is called _before_ <tt>Validations.validate</tt> (which is part of the <tt>Base.save</tt> call).
240
+ def before_validation() end
241
+
242
+ # Is called _after_ <tt>Validations.validate</tt> (which is part of the <tt>Base.save</tt> call).
243
+ def after_validation() end
244
+
245
+ # Is called _before_ <tt>Validations.validate</tt> (which is part of the <tt>Base.save</tt> call) on new objects
246
+ # that haven't been saved yet (no record exists).
247
+ def before_validation_on_create() end
248
+
249
+ # Is called _after_ <tt>Validations.validate</tt> (which is part of the <tt>Base.save</tt> call) on new objects
250
+ # that haven't been saved yet (no record exists).
251
+ def after_validation_on_create() end
252
+
253
+ # Is called _before_ <tt>Validations.validate</tt> (which is part of the <tt>Base.save</tt> call) on
254
+ # existing objects that have a record.
255
+ def before_validation_on_update() end
256
+
257
+ # Is called _after_ <tt>Validations.validate</tt> (which is part of the <tt>Base.save</tt> call) on
258
+ # existing objects that have a record.
259
+ def after_validation_on_update() end
260
+
261
+ def valid_with_callbacks? #:nodoc:
262
+ return false if callback(:before_validation) == false
263
+ if new_record? then result = callback(:before_validation_on_create) else result = callback(:before_validation_on_update) end
264
+ return false if result == false
265
+
266
+ result = valid_without_callbacks?
267
+
268
+ callback(:after_validation)
269
+ if new_record? then callback(:after_validation_on_create) else callback(:after_validation_on_update) end
270
+
271
+ return result
272
+ end
273
+
274
+ # Is called _before_ <tt>Base.destroy</tt>.
275
+ #
276
+ # Note: If you need to _destroy_ or _nullify_ associated records first,
277
+ # use the <tt>:dependent</tt> option on your associations.
278
+ def before_destroy() end
279
+
280
+ # Is called _after_ <tt>Base.destroy</tt> (and all the attributes have been frozen).
281
+ #
282
+ # class Contact < CouchFoo::Base
283
+ # after_destroy { |record| logger.info( "Contact #{record.id} was destroyed." ) }
284
+ # end
285
+ def after_destroy() end
286
+ def destroy_with_callbacks #:nodoc:
287
+ return false if callback(:before_destroy) == false
288
+ result = destroy_without_callbacks
289
+ callback(:after_destroy)
290
+ result
291
+ end
292
+
293
+ private
294
+ def callback(method)
295
+ notify(method)
296
+
297
+ result = run_callbacks(method) { |result, object| result == false }
298
+
299
+ if result != false && respond_to_without_attributes?(method)
300
+ result = send(method)
301
+ end
302
+
303
+ return result
304
+ end
305
+
306
+ def notify(method) #:nodoc:
307
+ self.class.changed
308
+ self.class.notify_observers(method, self)
309
+ end
310
+ end
311
+ end
@@ -0,0 +1,157 @@
1
+ require 'couchrest'
2
+
3
+ # This class wrappers CouchRest but may ultimately replace it as only parts of the library are used
4
+ module CouchFoo
5
+ LATEST_COUCHDB_VERSION = 0.9
6
+
7
+ class DatabaseWrapper
8
+
9
+ attr_accessor :database, :database_version, :bulk_save_default
10
+
11
+ def initialize(database_url, bulk_save_default, version, *args)
12
+ self.database = CouchRest.database(database_url)
13
+ self.bulk_save_default = bulk_save_default
14
+ self.database_version = version
15
+ end
16
+
17
+ def save(doc, bulk_save = bulk_save?)
18
+ begin
19
+ response = database.save(doc, bulk_save)
20
+ check_response_ok(response)
21
+ rescue Exception => e
22
+ handle_exception(e)
23
+ end
24
+ end
25
+
26
+ def delete(doc)
27
+ begin
28
+ response = database.delete(doc)
29
+ check_response_ok(response)
30
+ rescue Exception => e
31
+ handle_exception(e)
32
+ end
33
+ end
34
+
35
+ def commit
36
+ begin
37
+ response = database.bulk_save
38
+ check_response_ok(response)
39
+ rescue Exception => e
40
+ handle_exception(e)
41
+ end
42
+ end
43
+
44
+ def get(doc)
45
+ begin
46
+ database.get(doc)
47
+ rescue Exception => e
48
+ handle_exception(e)
49
+ end
50
+ end
51
+
52
+ def view(doc, params)
53
+ begin
54
+ database.view(doc, params)
55
+ rescue Exception => e
56
+ handle_exception(e)
57
+ end
58
+ end
59
+
60
+ def slow_view(doc, params)
61
+ begin
62
+ database.slow_view(doc, params)
63
+ rescue Exception => e
64
+ handle_exception(e)
65
+ end
66
+ end
67
+
68
+ # At the moment this is limited by the CouchREST bulk save limit of 50 transactions
69
+ def transaction(&block)
70
+ yield
71
+ commit
72
+ end
73
+
74
+ def bulk_save?
75
+ bulk_save_default
76
+ end
77
+
78
+ def version
79
+ database_version
80
+ end
81
+
82
+ private
83
+ # Checks the response is ok, raises generic exception if not
84
+ def check_response_ok(response)
85
+ if response["ok"]
86
+ response
87
+ else
88
+ logger.error("Unexpected response from database - #{response['ok']}")
89
+ raise CouchFooError, "Couldn't understand database response:#{response}"
90
+ end
91
+ end
92
+
93
+ # Raises appropriate exceptions based on error from server
94
+ def handle_exception(exception)
95
+ if exception.is_a?(RestClient::ResourceNotFound)
96
+ raise DocumentNotFound, "Couldn't find document"
97
+ elsif exception.is_a?(RestClient::RequestFailed) && exception.code == "409"
98
+ raise DocumentConflict, "Document has been updated whilst object loaded"
99
+ else
100
+ # We let the rest fall through as normally CouchDB setup error
101
+ raise exception
102
+ end
103
+ end
104
+ end
105
+
106
+ module Database
107
+ def self.included(base)
108
+ base.extend ClassMethods
109
+ base.cattr_accessor :bulk_save_default, :instance_writer => false
110
+ base.class_eval "@@bulk_save_default = false"
111
+ end
112
+
113
+ module ClassMethods
114
+ # Get the current database
115
+ def database
116
+ if @active_database.nil?
117
+ if self == CouchFoo::Base
118
+ raise Exception, "No databases setup"
119
+ else
120
+ superclass.database
121
+ end
122
+ else
123
+ @active_database
124
+ end
125
+ end
126
+
127
+ # Set the database to be used with this model. This honours inheritence so sub-classes can use
128
+ # different databases from their parents. As such if you only use one database for your
129
+ # application then only one call is required to CouchFoo::Base for initial setup.
130
+ #
131
+ # For ultra-scalability and using a different database for each user, perform the set_database
132
+ # call on the CouchFoo::Base object on a before_filter using the session information to
133
+ # determine the database to connect to. For example:
134
+ #
135
+ # class ApplicationController < ActionController::Base
136
+ # before_filter :set_user_database
137
+ #
138
+ # def set_user_database
139
+ # CouchFoo::Base.set_database("http://localhost:5984/user#{session[:user]}")
140
+ # end
141
+ # end
142
+ #
143
+ # As the need grows to move user databases onto different servers (sharding) then you can
144
+ # either:<ul>
145
+ # <li>create a lookup file/database that maps user_id to database location</li>
146
+ # <li>locate the database servers behind apache (or equivalent) using rewrite rules. The server
147
+ # knows which users live on which physical machine and rewrites accordingly. Thus only one
148
+ # database url is required at the application level)</li>
149
+ # </ul>
150
+ #
151
+ # NOTE: This will work best on domains where there is little overlap between users data (eg basecamp)
152
+ def set_database(url, version = LATEST_COUCHDB_VERSION, bulk_save = bulk_save_default)
153
+ @active_database = DatabaseWrapper.new(url, bulk_save, version)
154
+ end
155
+ end # ClassMethods
156
+ end
157
+ end