hyperactive 0.1.1 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -26,350 +26,375 @@ require 'archipelago'
26
26
  module Hyperactive
27
27
 
28
28
  #
29
- # The default database connector.
29
+ # The package containing the base class Bass that simplifies
30
+ # using archipelago for generic database stuff.
30
31
  #
31
- CAPTAIN = Archipelago::Pirate::Captain.new
32
-
33
- #
34
- # A tiny <b>call</b>able class that saves stuff in
35
- # indexes depending on certain attributes.
36
- #
37
- class IndexBuilder
38
- #
39
- # Get the first part of the key, that depends on the +attributes+.
40
- #
41
- def self.get_attribute_key_part(attributes)
42
- "Hyperactive::IndexBuilder::#{attributes.join(",")}"
43
- end
44
- #
45
- # Get the last part of the key, that depends on the +values+.
46
- #
47
- def self.get_value_key_part(values)
48
- "#{values.join(",")}"
49
- end
32
+ module Record
33
+
50
34
  #
51
- # Initialize an IndexBuilder giving it an array of +attributes+
52
- # that will be indexed.
35
+ # The default database connector.
53
36
  #
54
- def initialize(attributes)
55
- @attributes = attributes
56
- end
37
+ CAPTAIN = Archipelago::Pirate::Captain.new
38
+
57
39
  #
58
- # Get the Tree for the given +record+.
40
+ # A tiny <b>call</b>able class that saves stuff in
41
+ # indexes depending on certain attributes.
59
42
  #
60
- def get_tree_for(record)
61
- values = @attributes.collect do |att|
62
- if record.respond_to?(att)
63
- record.send(att)
64
- else
65
- nil
43
+ class IndexBuilder
44
+ #
45
+ # Get the first part of the key, that depends on the +attributes+.
46
+ #
47
+ def self.get_attribute_key_part(attributes)
48
+ "Hyperactive::IndexBuilder::#{attributes.join(",")}"
49
+ end
50
+ #
51
+ # Get the last part of the key, that depends on the +values+.
52
+ #
53
+ def self.get_value_key_part(values)
54
+ "#{values.join(",")}"
55
+ end
56
+ #
57
+ # Initialize an IndexBuilder giving it an array of +attributes+
58
+ # that will be indexed.
59
+ #
60
+ def initialize(attributes)
61
+ @attributes = attributes
62
+ end
63
+ #
64
+ # Get the Tree for the given +record+.
65
+ #
66
+ def get_key_for(record)
67
+ values = @attributes.collect do |att|
68
+ if record.respond_to?(att)
69
+ record.send(att)
70
+ else
71
+ nil
72
+ end
66
73
  end
74
+ key = "#{self.class.get_attribute_key_part(@attributes)}::#{self.class.get_value_key_part(values)}"
67
75
  end
68
- key = "#{self.class.get_attribute_key_part(@attributes)}::#{self.class.get_value_key_part(values)}"
69
- return CAPTAIN[key] ||= Tree.get_instance
70
- end
71
- #
72
- # Call this IndexBuilder and pass it a +block+.
73
- #
74
- # If the +argument+ is an Array then we know we are a save hook,
75
- # otherwise we are a destroy hook.
76
- #
77
- def call(argument, &block)
78
- yield
79
-
80
76
  #
81
- # If the argument is an Array (of old value, new value)
82
- # then we are a save hook, otherwise a destroy hook.
77
+ # Call this IndexBuilder and pass it a +block+.
83
78
  #
84
- if Array === argument
85
- record = argument.last
86
- old_record = argument.first
87
- get_tree_for(old_record).delete(record.record_id)
88
- get_tree_for(record)[record.record_id] = record
89
- else
90
- record = argument
91
- get_tree_for(record).delete(record.record_id)
79
+ # If the +argument+ is an Array then we know we are a save hook,
80
+ # otherwise we are a destroy hook.
81
+ #
82
+ def call(argument, &block)
83
+ yield
84
+
85
+ #
86
+ # If the argument is an Array (of old value, new value)
87
+ # then we are a save hook, otherwise a destroy hook.
88
+ #
89
+ if Array === argument
90
+ record = argument.last
91
+ old_record = argument.first
92
+ old_key = get_key_for(old_record)
93
+ new_key = get_key_for(record)
94
+ if old_key != new_key
95
+ (CAPTAIN[old_key] ||= Hyperactive::Tree::Root.get_instance).delete(record.record_id)
96
+ (CAPTAIN[new_key] ||= Hyperactive::Tree::Root.get_instance)[record.record_id] = record
97
+ end
98
+ else
99
+ record = argument
100
+ (CAPTAIN[get_key_for(record)] ||= Hyperactive::Tree::Root.get_instance).delete(record.record_id)
101
+ end
92
102
  end
93
103
  end
94
- end
95
104
 
96
- #
97
- # A tiny <b>call</b>able class that saves stuff inside containers
98
- # if they match certain criteria.
99
- #
100
- class MatchSaver
101
- #
102
- # Initialize this MatchSaver with a +key+, a <b>call</b>able +matcher+
103
- # and a +mode+ (:select, :reject, :delete_if_match or :delete_unless_match).
104
105
  #
105
- def initialize(key, matcher, mode)
106
- @key = key
107
- @matcher = matcher
108
- @mode = mode
109
- end
106
+ # A tiny <b>call</b>able class that saves stuff inside containers
107
+ # if they match certain criteria.
110
108
  #
111
- # Depending on <i>@mode</i> and return value of <i>@matcher</i>.call
112
- # may save record in the Hash-like container named <i>@key</i> in
113
- # the main database after having yielded to +block+.
114
- #
115
- def call(argument, &block)
116
- yield
109
+ class MatchSaver
110
+ #
111
+ # Initialize this MatchSaver with a +key+, a <b>call</b>able +matcher+
112
+ # and a +mode+ (:select, :reject, :delete_if_match or :delete_unless_match).
113
+ #
114
+ def initialize(key, matcher, mode)
115
+ @key = key
116
+ @matcher = matcher
117
+ @mode = mode
118
+ end
119
+ #
120
+ # Depending on <i>@mode</i> and return value of <i>@matcher</i>.call
121
+ # may save record in the Hash-like container named <i>@key</i> in
122
+ # the main database after having yielded to +block+.
123
+ #
124
+ def call(argument, &block)
125
+ yield
117
126
 
118
- record = argument
119
- record = argument.last if Array === argument
127
+ record = argument
128
+ record = argument.last if Array === argument
120
129
 
121
- case @mode
122
- when :select
123
- if @matcher.call(record)
124
- CAPTAIN[@key][record.record_id] = record
125
- else
126
- CAPTAIN[@key].delete(record.record_id)
127
- end
128
- when :reject
129
- if @matcher.call(record)
130
- CAPTAIN[@key].delete(record.record_id)
131
- else
132
- CAPTAIN[@key][record.record_id] = record
133
- end
134
- when :delete_if_match
135
- if @matcher.call(record)
136
- CAPTAIN[@key].delete(record.record_id)
137
- end
138
- when :delete_unless_match
139
- unless @matcher.call(record)
140
- CAPTAIN[@key].delete(record.record_id)
130
+ case @mode
131
+ when :select
132
+ if @matcher.call(record)
133
+ CAPTAIN[@key][record.record_id] = record
134
+ else
135
+ CAPTAIN[@key].delete(record.record_id)
136
+ end
137
+ when :reject
138
+ if @matcher.call(record)
139
+ CAPTAIN[@key].delete(record.record_id)
140
+ else
141
+ CAPTAIN[@key][record.record_id] = record
142
+ end
143
+ when :delete_if_match
144
+ if @matcher.call(record)
145
+ CAPTAIN[@key].delete(record.record_id)
146
+ end
147
+ when :delete_unless_match
148
+ unless @matcher.call(record)
149
+ CAPTAIN[@key].delete(record.record_id)
150
+ end
141
151
  end
142
152
  end
143
153
  end
144
- end
145
154
 
146
- #
147
- # A convenient base class to inherit when you want the basic utility methods
148
- # provided by for example ActiveRecord::Base *hint hint*.
149
- #
150
- # NB: After an instance is created, it will actually return a copy <b>within your local machine</b>
151
- # which is not what is usually the case. Every other time you fetch it using a select or other
152
- # method you will instead receive a proxy object to the database. This means that nothing you
153
- # do to it at that point will be persistent or even necessarily have a defined result.
154
- # Therefore: do not use <b>MyRecordSubclass.new</b> to <b>MyRecordSubclass#initialize</b> objects,
155
- # instead use <b>MyRecordSubclass.get_instance</b>, since it will return a fresh proxy object
156
- # instead of the devious original:
157
- # my_instance = MyRecordSubclass.get_instance(*the_same_arguments_as_to_initialize)
158
- #
159
- class Record
160
-
161
- @@create_hooks_by_class = {}
162
- @@destroy_hooks_by_class = {}
163
- @@save_hooks_by_class = {}
164
-
165
- #
166
- # The host we are running on.
167
155
  #
168
- HOST = "#{Socket::gethostbyname(Socket::gethostname)[0]}" rescue "localhost"
156
+ # A convenient base class to inherit when you want the basic utility methods
157
+ # provided by for example ActiveRecord::Base *hint hint*.
158
+ #
159
+ # NB: After an instance is created, it will actually return a copy <b>within your local machine</b>
160
+ # which is not what is usually the case. Every other time you fetch it using a select or other
161
+ # method you will instead receive a proxy object to the database. This means that nothing you
162
+ # do to it at that point will be persistent or even necessarily have a defined result.
163
+ # Therefore: do not use <b>MyRecordSubclass.new</b> to <b>MyRecordSubclass#initialize</b> objects,
164
+ # instead use <b>MyRecordSubclass.get_instance</b>, since it will return a fresh proxy object
165
+ # instead of the devious original:
166
+ # my_instance = MyRecordSubclass.get_instance(*the_same_arguments_as_to_initialize)
167
+ #
168
+ class Bass
169
+
170
+ @@create_hooks_by_class = {}
171
+ @@destroy_hooks_by_class = {}
172
+ @@save_hooks_by_class = {}
169
173
 
170
- #
171
- # Call this if you want to change the default database connector
172
- # to something else.
173
- #
174
- def self.setup(options = {})
175
- CAPTAIN.setup(options[:pirate_options])
176
- end
174
+ #
175
+ # The host we are running on.
176
+ #
177
+ HOST = "#{Socket::gethostbyname(Socket::gethostname)[0]}" rescue "localhost"
177
178
 
178
- #
179
- # Return our create_hooks, which can then be treated as any old Array.
180
- #
181
- # These must be <b>call</b>able objects with an arity of 1
182
- # that will be sent the instance about to be created (initial
183
- # insertion into the database system) that take a block argument.
184
- #
185
- # The block argument will be a Proc that actually injects the
186
- # instance into the database system.
187
- #
188
- # Use this to preprocess, validate and/or postprocess your
189
- # instances upon creation.
190
- #
191
- def self.create_hooks
192
- self.get_hook_array_by_class(@@create_hooks_by_class)
193
- end
179
+ #
180
+ # Call this if you want to change the default database connector
181
+ # to something else.
182
+ #
183
+ def self.setup(options = {})
184
+ CAPTAIN.setup(options[:pirate_options])
185
+ end
194
186
 
195
- #
196
- # Return our destroy_hooks, which can then be treated as any old Array.
197
- #
198
- # These must be <b>call</b>able objects with an arity of 1
199
- # that will be sent the instance about to be destroyed (removal
200
- # from the database system) that take a block argument.
201
- #
202
- # The block argument will be a Proc that actually removes the
203
- # instance from the database system.
204
- #
205
- # Use this to preprocess, validate and/or postprocess your
206
- # instances upon destruction.
207
- #
208
- def self.destroy_hooks
209
- self.get_hook_array_by_class(@@destroy_hooks_by_class)
210
- end
187
+ #
188
+ # Return our create_hooks, which can then be treated as any old Array.
189
+ #
190
+ # These must be <b>call</b>able objects with an arity of 1
191
+ # that will be sent the instance about to be created (initial
192
+ # insertion into the database system) that take a block argument.
193
+ #
194
+ # The block argument will be a Proc that actually injects the
195
+ # instance into the database system.
196
+ #
197
+ # Use this to preprocess, validate and/or postprocess your
198
+ # instances upon creation.
199
+ #
200
+ def self.create_hooks
201
+ self.get_hook_array_by_class(@@create_hooks_by_class)
202
+ end
211
203
 
212
- #
213
- # Return our save_hooks, which can then be treated as any old Array.
214
- #
215
- # These must be <b>call</b>able objects with an arity of 1
216
- # that will be sent [the old version, the new version] of the
217
- # instance about to be saved (storage into the database system)
218
- # along with a block argument.
219
- #
220
- # The block argument will be a Proc that actually saves the
221
- # instance into the database system.
222
- #
223
- # Use this to preprocess, validate and/or postprocess your
224
- # instances upon saving.
225
- #
226
- def self.save_hooks
227
- self.get_hook_array_by_class(@@save_hooks_by_class)
228
- end
204
+ #
205
+ # Return our destroy_hooks, which can then be treated as any old Array.
206
+ #
207
+ # These must be <b>call</b>able objects with an arity of 1
208
+ # that will be sent the instance about to be destroyed (removal
209
+ # from the database system) that take a block argument.
210
+ #
211
+ # The block argument will be a Proc that actually removes the
212
+ # instance from the database system.
213
+ #
214
+ # Use this to preprocess, validate and/or postprocess your
215
+ # instances upon destruction.
216
+ #
217
+ def self.destroy_hooks
218
+ self.get_hook_array_by_class(@@destroy_hooks_by_class)
219
+ end
220
+
221
+ #
222
+ # Return our save_hooks, which can then be treated as any old Array.
223
+ #
224
+ # These must be <b>call</b>able objects with an arity of 1
225
+ # that will be sent [the old version, the new version] of the
226
+ # instance about to be saved (storage into the database system)
227
+ # along with a block argument.
228
+ #
229
+ # The block argument will be a Proc that actually saves the
230
+ # instance into the database system.
231
+ #
232
+ # Use this to preprocess, validate and/or postprocess your
233
+ # instances upon saving.
234
+ #
235
+ def self.save_hooks
236
+ self.get_hook_array_by_class(@@save_hooks_by_class)
237
+ end
229
238
 
230
- def self.index_by(*attributes)
231
- attribute_key_part = IndexBuilder.get_attribute_key_part(attributes)
232
- self.class_eval <<END
239
+ #
240
+ # Create an index for this class.
241
+ #
242
+ # Will create a method find_by_#{attributes.join("_and_")} for this
243
+ # class that will return what you expect.
244
+ #
245
+ def self.index_by(*attributes)
246
+ attribute_key_part = IndexBuilder.get_attribute_key_part(attributes)
247
+ self.class_eval <<END
233
248
  def self.find_by_#{attributes.join("_and_")}(*args)
234
- key = "#{attribute_key_part}::" + IndexBuilder.get_value_key_part(args)
235
- CAPTAIN[key]
249
+ key = "#{attribute_key_part}::" + IndexBuilder.get_value_key_part(args)
250
+ CAPTAIN[key]
236
251
  end
237
252
  END
238
- index_builder = IndexBuilder.new(attributes)
239
- self.save_hooks << index_builder
240
- self.destroy_hooks << index_builder
241
- end
253
+ index_builder = IndexBuilder.new(attributes)
254
+ self.save_hooks << index_builder
255
+ self.destroy_hooks << index_builder
256
+ end
242
257
 
243
- #
244
- # Will define a method called +name+ that will include all
245
- # existing instances of this class that when sent to +matcher+.call
246
- # return true. Will only return instances saved after this selector
247
- # is defined.
248
- #
249
- def self.select(name, matcher)
250
- key = self.collection_key(name)
251
- CAPTAIN[key] ||= Tree.get_instance
252
- self.class_eval <<END
258
+ #
259
+ # Will define a method called +name+ that will include all
260
+ # existing instances of this class that when sent to +matcher+.call
261
+ # return true. Will only return instances saved after this selector
262
+ # is defined.
263
+ #
264
+ def self.select(name, &block)
265
+ key = self.collection_key(name)
266
+ CAPTAIN[key] ||= Hyperactive::Tree::Root.get_instance
267
+ self.class_eval <<END
253
268
  def self.#{name}
254
269
  CAPTAIN["#{key}"]
255
270
  end
256
271
  END
257
- self.save_hooks << MatchSaver.new(key, matcher, :select)
258
- self.destroy_hooks << MatchSaver.new(key, matcher, :delete_if_match)
259
- end
272
+ self.save_hooks << MatchSaver.new(key, Proc.new do |arg|
273
+ yield(arg)
274
+ end, :select)
275
+ self.destroy_hooks << MatchSaver.new(key, Proc.new do |arg|
276
+ yield(arg)
277
+ end, :delete_if_match)
278
+ end
260
279
 
261
- #
262
- # Will define a method called +name+ that will include all
263
- # existing instances of this class that when sent to +matcher+.call
264
- # does not return true. Will only return instances saved after this
265
- # rejector is defined.
266
- #
267
- def self.reject(name, matcher)
268
- key = self.collection_key(name)
269
- CAPTAIN[key] ||= Tree.get_instance
270
- self.class_eval <<END
280
+ #
281
+ # Will define a method called +name+ that will include all
282
+ # existing instances of this class that when sent to +matcher+.call
283
+ # does not return true. Will only return instances saved after this
284
+ # rejector is defined.
285
+ #
286
+ def self.reject(name, &block)
287
+ key = self.collection_key(name)
288
+ CAPTAIN[key] ||= Hyperactive::Tree::Root.get_instance
289
+ self.class_eval <<END
271
290
  def self.#{name}
272
291
  CAPTAIN["#{key}"]
273
292
  end
274
293
  END
275
- self.save_hooks << MatchSaver.new(key, matcher, :reject)
276
- self.destroy_hooks << MatchSaver.new(key, matcher, :delete_unless_match)
277
- end
294
+ self.save_hooks << MatchSaver.new(key, Proc.new do |arg|
295
+ yield(arg)
296
+ end, :reject)
297
+ self.destroy_hooks << MatchSaver.new(key, Proc.new do |arg|
298
+ yield(arg)
299
+ end, :delete_unless_match)
300
+ end
278
301
 
279
- #
280
- # Return the record with +record_id+.
281
- #
282
- def self.find(record_id)
283
- CAPTAIN[record_id]
284
- end
302
+ #
303
+ # Return the record with +record_id+.
304
+ #
305
+ def self.find(record_id)
306
+ CAPTAIN[record_id]
307
+ end
285
308
 
286
- #
287
- # Will execute +block+ within a transaction.
288
- #
289
- def self.transaction(&block)
290
- CAPTAIN.transaction(&block)
291
- end
292
-
293
- #
294
- # Use this method to get new instances of this class, since it will actually
295
- # make sure it both resides in the database and return a proxy to the remote
296
- # object.
297
- #
298
- def self.get_instance(*arguments)
299
- instance = self.new(*arguments)
300
- instance.instance_eval do
301
- @record_id = Digest::SHA1.hexdigest("#{HOST}:#{Time.new.to_f}:#{self.object_id}:#{rand(1 << 32)}")
309
+ #
310
+ # Will execute +block+ within a transaction.
311
+ #
312
+ def self.transaction(&block)
313
+ CAPTAIN.transaction(&block)
302
314
  end
303
315
 
304
- Hyperactive::Hooker.call_with_hooks(instance, *self.create_hooks) do
305
- CAPTAIN[instance.record_id] = instance
316
+ #
317
+ # Use this method to get new instances of this class, since it will actually
318
+ # make sure it both resides in the database and return a proxy to the remote
319
+ # object.
320
+ #
321
+ def self.get_instance(*arguments)
322
+ instance = self.new(*arguments)
323
+ instance.instance_eval do
324
+ @record_id = Digest::SHA1.hexdigest("#{HOST}:#{Time.new.to_f}:#{self.object_id}:#{rand(1 << 32)}")
325
+ end
326
+
327
+ Hyperactive::Hooker.call_with_hooks(instance, *self.create_hooks) do
328
+ CAPTAIN[instance.record_id] = instance
329
+ end
330
+
331
+ proxy = CAPTAIN[instance.record_id]
332
+
333
+ return proxy
306
334
  end
307
-
308
- proxy = CAPTAIN[instance.record_id]
309
-
310
- return proxy
311
- end
312
-
313
- #
314
- # Our semi-unique id.
315
- #
316
- attr_reader :record_id
317
-
318
- #
319
- # This will allow us to wrap any write of us to persistent storage
320
- # in the @@save_hooks as long as the Archipelago::Hashish provider
321
- # supports it. See Archipelago::Hashish::BerkeleyHashish for an example
322
- # of Hashish providers that do this.
323
- #
324
- def save_hook(old_value, &block)
325
- Hyperactive::Hooker.call_with_hooks([old_value, self], *self.class.save_hooks) do
326
- yield
335
+
336
+ #
337
+ # Our semi-unique id.
338
+ #
339
+ attr_reader :record_id
340
+
341
+ #
342
+ # This will allow us to wrap any write of us to persistent storage
343
+ # in the @@save_hooks as long as the Archipelago::Hashish provider
344
+ # supports it. See Archipelago::Hashish::BerkeleyHashish for an example
345
+ # of Hashish providers that do this.
346
+ #
347
+ def save_hook(old_value, &block)
348
+ Hyperactive::Hooker.call_with_hooks([old_value, self], *self.class.save_hooks) do
349
+ yield
350
+ end
327
351
  end
328
- end
329
-
330
- #
331
- # Remove this instance from the database calling all the right hooks.
332
- #
333
- # Freezes this instance after having deleted it.
334
- #
335
- # Returns false without destroying anything if any of the @@pre_destroy_hooks
336
- # returns false.
337
- #
338
- # Returns true otherwise.
339
- #
340
- def destroy
341
- Hyperactive::Hooker.call_with_hooks(self, *self.class.destroy_hooks) do
342
- CAPTAIN.delete(@record_id)
343
- self.freeze
352
+
353
+ #
354
+ # Remove this instance from the database calling all the right hooks.
355
+ #
356
+ # Freezes this instance after having deleted it.
357
+ #
358
+ # Returns false without destroying anything if any of the @@pre_destroy_hooks
359
+ # returns false.
360
+ #
361
+ # Returns true otherwise.
362
+ #
363
+ def destroy
364
+ Hyperactive::Hooker.call_with_hooks(self, *self.class.destroy_hooks) do
365
+ CAPTAIN.delete(@record_id)
366
+ self.freeze
367
+ end
344
368
  end
345
- end
346
-
347
- private
348
-
349
- #
350
- # The key used to store the collection with the given +sym+ as name.
351
- #
352
- def self.collection_key(sym)
353
- "Hyperactive::Record::collection_key::#{sym}"
354
- end
355
-
356
- #
357
- # Get an Array from +hash+ using <i>self</i> as
358
- # key. If <i>self</i> doesnt exist in the +hash+
359
- # it will recurse by calling the same method in the
360
- # superclass until it has been called in Hyperactive::Record.
361
- #
362
- def self.get_hook_array_by_class(hash)
363
- return hash[self] if hash.include?(self)
364
-
365
- if self == Record
366
- hash[self] = []
367
- return self.get_hook_array_by_class(hash)
368
- else
369
- hash[self] = self.superclass.get_hook_array_by_class(hash).clone
370
- return self.get_hook_array_by_class(hash)
369
+
370
+ private
371
+
372
+ #
373
+ # The key used to store the collection with the given +sym+ as name.
374
+ #
375
+ def self.collection_key(sym)
376
+ "Hyperactive::Record::Bass::collection_key::#{sym}"
371
377
  end
378
+
379
+ #
380
+ # Get an Array from +hash+ using <i>self</i> as
381
+ # key. If <i>self</i> doesnt exist in the +hash+
382
+ # it will recurse by calling the same method in the
383
+ # superclass until it has been called in Hyperactive::Record::Base.
384
+ #
385
+ def self.get_hook_array_by_class(hash)
386
+ return hash[self] if hash.include?(self)
387
+
388
+ if self == Bass
389
+ hash[self] = []
390
+ return self.get_hook_array_by_class(hash)
391
+ else
392
+ hash[self] = self.superclass.get_hook_array_by_class(hash).clone
393
+ return self.get_hook_array_by_class(hash)
394
+ end
395
+ end
396
+
372
397
  end
373
-
374
398
  end
375
399
  end
400
+