hyperactive 0.2.3 → 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
data/README CHANGED
@@ -7,15 +7,21 @@ It uses archipelago for persistence, and is meaningful only in an environment wh
7
7
  Hyperactive::Record:: The base class package itself, providing you with cached selectors and rejectors, along with cached finders for any number of attributes.
8
8
  Hyperactive::Hash:: A collection class that contains any number of Hyperactive::Records in a hash like structure.
9
9
  Hyperactive::List:: A collection class that contains any number of Hyperactive::Records in a list like structure.
10
+ Hyperactive::Index:: A module included into Hyperactive::Record that adds simple indexing capabilities of the <b>find_by_...</b> type.
11
+ Hyperactive::Transactions:: A module included into Hyperactive::Record that adds a few simplifications in handling transactions properly.
10
12
 
11
13
  == Usage:
12
14
 
13
15
  To use Hyperactive in the simplest way, just run the script/services.rb script from the Archipelago
14
16
  distribution to get a few services running, then subclass Hyperactive::Record::Bass and create
15
- objects with <b>MyBassSubclass.get_instance</b> (not <b>new</b>!). They will be automagically
16
- stored in the network, and fetchable via their <b>instance.record_id</b>.
17
+ objects with <b>MyBassSubclass.get_instance</b>. They will be automagically stored in the network,
18
+ and fetchable via their <b>instance.record_id</b>.
17
19
 
18
- By loading Hyperactive::Record you will automatically define Hyperactive::Record::CAPTAIN which will
20
+ You can also use <b>new</b> of course, but beware that this will not store the object persistently
21
+ <b>or</b> give you a proxy. Both of these things are good for you, so use get_instance unless you know
22
+ what you are doing.
23
+
24
+ By loading Hyperactive::Record you will automatically define Hyperactive::CAPTAIN which will
19
25
  be an Archipelago::Pirate::Captain that is your interface to the distributed database.
20
26
 
21
27
  == Examples:
data/TODO ADDED
@@ -0,0 +1,4 @@
1
+
2
+ * Create a validation framework for Hyperactive::Record::Bass
3
+
4
+ * Create a sorted data structure, for example AA trees: http://www.eternallyconfuzzled.com/tuts/andersson.html
@@ -62,6 +62,7 @@ module Hyperactive
62
62
  # NB: Remember to call create on the new instance or use Head.get_instance to get that done automatically.
63
63
  #
64
64
  def initialize
65
+ super
65
66
  self.list = nil
66
67
  end
67
68
 
@@ -72,11 +73,18 @@ module Hyperactive
72
73
  self.list.size
73
74
  end
74
75
 
76
+ #
77
+ # Return whether this Hash is empty.
78
+ #
79
+ def empty?
80
+ self.list.empty?
81
+ end
82
+
75
83
  #
76
84
  # Return the value for +key+.
77
85
  #
78
86
  def [](key)
79
- element = Hyperactive::Record::CAPTAIN[my_key_for(key), @transaction]
87
+ element = Hyperactive::CAPTAIN[my_key_for(key), @transaction]
80
88
  if element
81
89
  return element.value
82
90
  else
@@ -88,7 +96,7 @@ module Hyperactive
88
96
  # Returns whether +key+ is included in this Hash.
89
97
  #
90
98
  def include?(key)
91
- Hyperactive::Record::CAPTAIN.include?(my_key_for(key), @transaction)
99
+ Hyperactive::CAPTAIN.include?(my_key_for(key), @transaction)
92
100
  end
93
101
 
94
102
  #
@@ -97,13 +105,13 @@ module Hyperactive
97
105
  def []=(key, value)
98
106
  self.list = Hyperactive::List::Head.get_instance_with_transaction(@transaction) unless self.list
99
107
 
100
- if (element = Hyperactive::Record::CAPTAIN[my_key_for(key), @transaction])
108
+ if (element = Hyperactive::CAPTAIN[my_key_for(key), @transaction])
101
109
  element.value = value
102
110
  else
103
111
  element = Element.get_instance_with_transaction(@transaction, key, value, nil)
104
112
  self.list << element
105
113
  element.list_element = self.list.last_element
106
- Hyperactive::Record::CAPTAIN[my_key_for(key), @transaction] = element
114
+ Hyperactive::CAPTAIN[my_key_for(key), @transaction] = element
107
115
  end
108
116
  end
109
117
 
@@ -115,9 +123,9 @@ module Hyperactive
115
123
 
116
124
  return_value = nil
117
125
 
118
- element = Hyperactive::Record::CAPTAIN[my_key_for(key), @transaction]
126
+ element = Hyperactive::CAPTAIN[my_key_for(key), @transaction]
119
127
  if element
120
- Hyperactive::Record::CAPTAIN.delete(my_key_for(key), @transaction)
128
+ Hyperactive::CAPTAIN.delete(my_key_for(key), @transaction)
121
129
  self.list.unlink!(element.list_element)
122
130
  return_value = element.value
123
131
  element.destroy!
@@ -19,61 +19,171 @@
19
19
  module Hyperactive
20
20
 
21
21
  #
22
- # A utility method to simplify using around-hooks for any block.
22
+ # A container of utility hook methods.
23
23
  #
24
24
  module Hooker
25
25
 
26
26
  #
27
- # Will call each hook in +hooks+ with a block that
28
- # calls the rest of the +hooks+ with +argument+ as argument
29
- # and after that yields to the given +block+.
27
+ # Something that uses hooks a lot, include if you want support for various types of hooks.
30
28
  #
31
- # This will allow you wrap any method call in a dynamic set
32
- # of other methods.
33
- #
34
- # For more examples, see Hyperactive::Record.
35
- #
36
- # Example:
37
- # class ImportantStuff
38
- # attr_accessor :timestamp
39
- # def is_valid?
40
- # # do nifty stuff
41
- # end
42
- # def notify_someone_that_i_am_changed!(message)
43
- # # do even more nifty stuff
44
- # end
45
- # def validate_and_notify(message)
46
- # raise "i am invalid!" unless is_valid?
47
- # yield
48
- # notify_someone_that_i_am_changed!(message)
49
- # end
50
- # def do_save
51
- # # save me in persistent storage
52
- # end
53
- # end
54
- # class ImportantHandler
55
- # def update_timestamp(important_stuff_instance)
56
- # important_stuff_instance.timestamp = Time.now
57
- # Hyperactive::Hooker.call_with_hooks("changed at #{Time.now}",
58
- # important_stuff_instance.method(:validate_and_notify)) do
59
- # important_stuff_instance.do_save
60
- # end
61
- # end
62
- # end
63
- #
64
- def call_with_hooks(argument, *hooks, &block)
65
- if hooks.empty?
66
- yield
67
- else
68
- first = hooks.first
69
- rest = hooks[1..-1]
70
- first.call(argument) do
71
- call_with_hooks(argument, *rest, &block)
29
+ module Pimp
30
+
31
+ #
32
+ # Upon inclusion, our +base+ class will be given a set of hook
33
+ # arrays, and then get extended by ClassMethods.
34
+ #
35
+ def self.append_features(base)
36
+ super
37
+ base.instance_variable_set(:@create_hooks, [])
38
+ base.instance_variable_set(:@destroy_hooks, [])
39
+ base.instance_variable_set(:@save_hooks, [])
40
+ base.instance_variable_set(:@load_hooks, [])
41
+ base.extend(ClassMethods)
42
+ end
43
+
44
+ #
45
+ # This will allow us to wrap any write of us to persistent storage
46
+ # in the @save_hooks as long as the Archipelago::Hashish provider
47
+ # supports it. See Archipelago::Hashish::BerkeleyHashish for an example
48
+ # of Hashish providers that do this.
49
+ #
50
+ def save_hook(old_value, &block)
51
+ self.class.with_hooks(:instance => self, :arguments => [old_value], :hooks => self.class.save_hooks) do
52
+ yield
72
53
  end
73
54
  end
74
- end
55
+
56
+ #
57
+ # This will allow us to wrap any load of us from persistent storage
58
+ # in the @@load_hooks as long as the Archipelago::Hashish provider
59
+ # supports it. See Archipelago::Hashish::BerkeleyHashish for an example
60
+ # of Hashish providers that do this.
61
+ #
62
+ def load_hook(&block)
63
+ self.class.with_hooks(:instance => self, :hooks => self.class.load_hooks) do
64
+ yield
65
+ end
66
+ end
67
+
75
68
 
76
- module_function :call_with_hooks
69
+ #
70
+ # The class methods of a Pimp.
71
+ #
72
+ module ClassMethods
73
+
74
+ #
75
+ # Will wrap a block within nested calls to given hooks.
76
+ #
77
+ # Parameters:
78
+ # * <i>:instance</i>: An instance to send methods to, defaults to self
79
+ # * <i>:arguments</i>: Extra arguments to send to whatever we call, defaults to self
80
+ # * <i>:hooks</i>: An Array of hook objects. They can be either Strings or Symbols, or
81
+ # whatever else can be given to <b>respond_to?</b>, <b>or</b> they can be anything
82
+ # that <b>respond_to?(:call)</b>. If the hook is something that
83
+ # <i>respond_to?(:call)</i> it will be <i>call</i>ed and given
84
+ # the the <i>:instance</i> and *<i>:arguments</i> and the provided +block+.
85
+ # Else the <i>:instance</i>.respond_to?(<i>the given hook</i>)
86
+ # must be true, and that <i>:instance</i> will be sent the <i>given hook</i> along with the <i>:arguments</i> and
87
+ # a block that does the rest of the hooks.
88
+ #
89
+ def with_hooks(options = {}, &block)
90
+ instance = options[:instance] || self
91
+ arguments = options[:arguments] || []
92
+ hooks = options[:hooks] || []
93
+
94
+ if hooks.empty?
95
+ yield
96
+ else
97
+ first = hooks.first
98
+ rest = hooks[1..-1]
99
+ if first.respond_to?(:call)
100
+ first.call(instance, *arguments) do
101
+ with_hooks(:instance => instance, :hooks => rest, :arguments => arguments, &block)
102
+ end
103
+ elsif instance.respond_to?(first)
104
+ instance.send(first, *arguments) do
105
+ with_hooks(:instance => instance, :hooks => rest, :arguments => arguments, &block)
106
+ end
107
+ else
108
+ raise "You can only send in hooks that are Symbols in the given instance or call'able objects"
109
+ end
110
+ end
111
+ end
112
+
113
+ #
114
+ # Upon inheritance, our +subclass+ will be given copies of our hooks.
115
+ #
116
+ def inherited(subclass)
117
+ subclass.instance_variable_set(:@create_hooks, @create_hooks.clone)
118
+ subclass.instance_variable_set(:@destroy_hooks, @destroy_hooks.clone)
119
+ subclass.instance_variable_set(:@save_hooks, @save_hooks.clone)
120
+ subclass.instance_variable_set(:@load_hooks, @load_hooks.clone)
121
+ end
122
+
123
+ #
124
+ # Return our create_hooks, which can then be treated as any old Array.
125
+ #
126
+ # These must be <b>call</b>able objects with an arity of 1
127
+ # that will be sent the instance about to be created (initial
128
+ # insertion into the database system) that take a block argument.
129
+ #
130
+ # The block argument will be a Proc that actually injects the
131
+ # instance into the database system.
132
+ #
133
+ # Use this to preprocess, validate and/or postprocess your
134
+ # instances upon creation.
135
+ #
136
+ attr_reader :create_hooks
137
+
138
+ #
139
+ # Return our destroy_hooks, which can then be treated as any old Array.
140
+ #
141
+ # These must be <b>call</b>able objects with an arity of 1
142
+ # that will be sent the instance about to be destroyed (removal
143
+ # from the database system) that take a block argument.
144
+ #
145
+ # The block argument will be a Proc that actually removes the
146
+ # instance from the database system.
147
+ #
148
+ # Use this to preprocess, validate and/or postprocess your
149
+ # instances upon destruction.
150
+ #
151
+ attr_reader :destroy_hooks
152
+
153
+ #
154
+ # Return our load_hooks, which can then be treated as any old Array.
155
+ #
156
+ # These must be <b>call</b>able objects with an arity of 1
157
+ # that will be sent the instance about to be loaded (insertion
158
+ # into the live hash of the database in question) that take a block argument.
159
+ #
160
+ # The block argument will be a Proc that actually puts the
161
+ # instance into the live hash.
162
+ #
163
+ # Use this to preprocess, validate and/or postprocess your
164
+ # instances upon loading.
165
+ #
166
+ attr_reader :load_hooks
167
+
168
+ #
169
+ # Return our save_hooks, which can then be treated as any old Array.
170
+ #
171
+ # These must be <b>call</b>able objects with an arity of 1
172
+ # that will be sent [the old version, the new version] of the
173
+ # instance about to be saved (storage into the database system)
174
+ # along with a block argument.
175
+ #
176
+ # The block argument will be a Proc that actually saves the
177
+ # instance into the database system.
178
+ #
179
+ # Use this to preprocess, validate and/or postprocess your
180
+ # instances upon saving.
181
+ #
182
+ attr_reader :save_hooks
183
+
184
+ end
185
+
186
+ end
77
187
 
78
188
  end
79
189
 
@@ -0,0 +1,210 @@
1
+ # Archipelago - a distributed computing toolkit for ruby
2
+ # Copyright (C) 2006 Martin Kihlgren <zond at troja dot ath dot cx>
3
+ #
4
+ # This program is free software; you can redistribute it and/or
5
+ # modify it under the terms of the GNU General Public License
6
+ # as published by the Free Software Foundation; either version 2
7
+ # of the License, or (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program; if not, write to the Free Software
16
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
+
18
+ module Hyperactive
19
+
20
+ module Index
21
+
22
+ #
23
+ # A tiny <b>call</b>able class that saves stuff in
24
+ # indexes depending on certain attributes.
25
+ #
26
+ class IndexBuilder
27
+ #
28
+ # Get the key for the given attributes and values.
29
+ #
30
+ def self.get_key(klass, attributes, values)
31
+ "Hyperactive::IndexBuilder::#{klass}::#{attributes.join(",")}::#{values.join(",")}"
32
+ end
33
+ #
34
+ # Initialize an IndexBuilder giving it an array of +attributes+
35
+ # that will be indexed.
36
+ #
37
+ def initialize(klass, attributes)
38
+ @klass = klass
39
+ @attributes = attributes
40
+ end
41
+ #
42
+ # Get the key for the given +record+.
43
+ #
44
+ def get_key_for(record)
45
+ values = @attributes.collect do |att|
46
+ if record.respond_to?(att)
47
+ record.send(att)
48
+ else
49
+ nil
50
+ end
51
+ end
52
+ key = self.class.get_key(@klass, @attributes, values)
53
+ end
54
+ #
55
+ # Call this IndexBuilder and pass it a +block+.
56
+ #
57
+ # If the +argument+ is an Array then we know we are a save hook,
58
+ # otherwise we are a destroy hook.
59
+ #
60
+ def call(*arguments, &block)
61
+ yield
62
+
63
+ #
64
+ # If the argument is an Array (of old value, new value)
65
+ # then we are a save hook, otherwise a destroy hook.
66
+ #
67
+ if arguments.size > 1
68
+ record = arguments.first
69
+ old_record = arguments.last
70
+ old_key = get_key_for(old_record)
71
+ new_key = get_key_for(record)
72
+ if old_key != new_key
73
+ (CAPTAIN[old_key] ||= Hyperactive::Hash::Head.get_instance_with_transaction(record.transaction)).delete(record.record_id)
74
+ (CAPTAIN[new_key] ||= Hyperactive::Hash::Head.get_instance_with_transaction(record.transaction))[record.record_id] = record
75
+ end
76
+ else
77
+ record = arguments.first
78
+ (CAPTAIN[get_key_for(record)] ||= Hyperactive::Hash::Head.get_instance_with_transaction(record.transaction)).delete(record.record_id)
79
+ end
80
+ end
81
+ end
82
+
83
+ #
84
+ # A tiny <b>call</b>able class that saves stuff inside containers
85
+ # if they match certain criteria.
86
+ #
87
+ class MatchSaver
88
+ #
89
+ # Initialize this MatchSaver with a +key+, a <b>call</b>able +matcher+
90
+ # and a +mode+ (:select, :reject, :delete_if_match or :delete_unless_match).
91
+ #
92
+ def initialize(key, matcher, mode)
93
+ @key = key
94
+ @matcher = matcher
95
+ @mode = mode
96
+ end
97
+ #
98
+ # Depending on <i>@mode</i> and return value of <i>@matcher</i>.call
99
+ # may save record in the Hash-like container named <i>@key</i> in
100
+ # the main database after having yielded to +block+.
101
+ #
102
+ def call(*arguments, &block)
103
+ yield
104
+
105
+ record = arguments.first
106
+
107
+ case @mode
108
+ when :select
109
+ if @matcher.call(record)
110
+ CAPTAIN[@key, record.transaction][record.record_id] = record
111
+ else
112
+ CAPTAIN[@key, record.transaction].delete(record.record_id)
113
+ end
114
+ when :reject
115
+ if @matcher.call(record)
116
+ CAPTAIN[@key, record.transaction].delete(record.record_id)
117
+ else
118
+ CAPTAIN[@key, record.transaction][record.record_id] = record
119
+ end
120
+ when :delete_if_match
121
+ if @matcher.call(record)
122
+ CAPTAIN[@key, record.transaction].delete(record.record_id)
123
+ end
124
+ when :delete_unless_match
125
+ unless @matcher.call(record)
126
+ CAPTAIN[@key, record.transaction].delete(record.record_id)
127
+ end
128
+ end
129
+ end
130
+ end
131
+
132
+ module Indexable
133
+
134
+ def self.append_features(base)
135
+ super
136
+ base.extend(ClassMethods)
137
+ end
138
+
139
+ module ClassMethods
140
+ #
141
+ # Create an index for this class.
142
+ #
143
+ # Will create a method find_by_#{attributes.join("_and_")} for this
144
+ # class that will return what you expect.
145
+ #
146
+ def index_by(*attributes)
147
+ klass = self
148
+ self.class.class_eval do
149
+ define_method("find_by_#{attributes.join("_and_")}") do |*args|
150
+ CAPTAIN[IndexBuilder.get_key(klass, attributes, args)]
151
+ end
152
+ end
153
+ index_builder = IndexBuilder.new(self, attributes)
154
+ self.save_hooks << index_builder
155
+ self.destroy_hooks << index_builder
156
+ end
157
+
158
+ #
159
+ # Will define a method called +name+ that will include all
160
+ # existing instances of this class that when yielded to the +block+
161
+ # returns true. Will only return instances saved after this selector
162
+ # is defined.
163
+ #
164
+ def select(name, &block) #:yields: instance
165
+ key = collection_key(name)
166
+ CAPTAIN[key] ||= Hyperactive::Hash::Head.get_instance
167
+ self.class.class_eval do
168
+ define_method(name) do
169
+ CAPTAIN[key]
170
+ end
171
+ end
172
+ self.save_hooks << MatchSaver.new(key, block, :select)
173
+ self.destroy_hooks << MatchSaver.new(key, block, :delete_if_match)
174
+ end
175
+
176
+ #
177
+ # Will define a method called +name+ that will include all
178
+ # existing instances of this class that when yielded to +block+
179
+ # does not return true. Will only return instances saved after this
180
+ # rejector is defined.
181
+ #
182
+ def reject(name, &block) #:yields: instance
183
+ key = collection_key(name)
184
+ CAPTAIN[key] ||= Hyperactive::Hash::Head.get_instance
185
+ self.class.class_eval do
186
+ define_method(name) do
187
+ CAPTAIN[key]
188
+ end
189
+ end
190
+ self.save_hooks << MatchSaver.new(key, block, :reject)
191
+ self.destroy_hooks << MatchSaver.new(key, block, :delete_unless_match)
192
+ end
193
+
194
+ private
195
+
196
+ #
197
+ # The key used to store the collection with the given +sym+ as name.
198
+ #
199
+ def collection_key(sym)
200
+ "Hyperactive::Index::Indexable::collection_key::#{sym}"
201
+ end
202
+
203
+
204
+ end
205
+
206
+ end
207
+
208
+ end
209
+
210
+ end
@@ -70,6 +70,7 @@ module Hyperactive
70
70
  # NB: Remember to call create on the new instance or use Head.get_instance to get that done automatically.
71
71
  #
72
72
  def initialize
73
+ super
73
74
  self.size = 0
74
75
  self.first_element = self.last_element = nil
75
76
  end
@@ -102,6 +103,13 @@ module Hyperactive
102
103
  element.destroy!
103
104
  end
104
105
 
106
+ #
107
+ # Return whether this List is empty.
108
+ #
109
+ def empty?
110
+ return self.size == 0
111
+ end
112
+
105
113
  #
106
114
  # Remove all elements from this List::Head.
107
115
  #