hyperactive 0.1.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+