message_router 0.0.2 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/.rvmrc CHANGED
@@ -1 +1 @@
1
- rvm use --create --install 1.8.7@message_router
1
+ rvm use --install ree
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- message_router (0.0.2)
4
+ message_router (0.1.1)
5
5
 
6
6
  GEM
7
7
  remote: http://rubygems.org/
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2009 Brad Gessler
1
+ Copyright (c) 2009-2012 Brad Gessler
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.rdoc CHANGED
@@ -1,6 +1,6 @@
1
1
  = Message Router
2
2
 
3
- Message router is a Sinatra-like DSL for processing simple messages, like SMS messages or Tweets.
3
+ Message router is a DSL for routing and processing simple messages, like SMS messages or Tweets.
4
4
 
5
5
  == Installation
6
6
 
@@ -8,54 +8,166 @@ Message router is a Sinatra-like DSL for processing simple messages, like SMS me
8
8
 
9
9
  == Example Code
10
10
 
11
- This is a contrived example for a Twitter message router, but it gives you an idea of how it works.
11
+ See rdoc for MessageRouter::Router.build (lib/message_router/router.rb) for examples.
12
12
 
13
- class TwitterRouter < MessageRouter
14
- context :all_caps_with_numbers do
15
- # All caps without numbers... but in a proc
16
- context Proc.new{|r| r.message[:body] =~ /^[A-Z\s]+$/ } do
17
- match /.+/ do
18
- "STOP SHOUTING WITHOUT NUMBERS!"
19
- end
20
- end
21
-
22
- match /.+/ do
23
- "STOP SHOUTING WITH NUMBERS!"
24
- end
13
+ And now some irb action.
14
+
15
+ 1.8.7 :001 > class HelloRouter < MessageRouter::Router
16
+ 1.8.7 :002?> match /hi/ do
17
+ 1.8.7 :003 > puts "Hi there. You sent me: #{env.inspect}"
18
+ 1.8.7 :004?> true # puts returns nil, and that would fail the matcher
19
+ 1.8.7 :005?> end
20
+ 1.8.7 :006?> end
21
+ => [[/hi/, #<Proc:0x00000000026963b8@(irb):2>]]
22
+ 1.8.7 :007 > HelloRouter.new.call({'body' => 'can you say hi to me?'})
23
+ Hi there. You sent me: {'body'=>"can you say hi to me?"}
24
+ => true
25
+ 1.8.7 :008 > class MainRouter < MessageRouter::Router
26
+ 1.8.7 :009?> match({'to' => 'greeter'}, HelloRouter.new)
27
+ 1.8.7 :010?> match(true) do
28
+ 1.8.7 :011 > puts "WTF? I don't know how to do that!"
29
+ 1.8.7 :012?> true # puts returns nil, and that would fail the matcher
30
+ 1.8.7 :013?> end
31
+ 1.8.7 :014?> end
32
+ => [[{:to=>"greeter"}, #<HelloRouter:0x25fef90 @rules=[[#<Proc:0x000000000266b2d0@/home/paul/sc/pe/message_router/lib/message_router/router.rb:165>, #<Proc:0x00000000026963b8@(irb):2>]]>], [true, #<Proc:0x00000000025ff580@(irb):10>]]
33
+ 1.8.7 :015 > MainRouter.new.call({'body' => 'can you say hi to me?'})
34
+ WTF? I don't know how to do that!
35
+ => true
36
+ 1.8.7 :016 > MainRouter.new.call({'body' => 'can you say hi to me?', 'to' => 'greeter'})
37
+ Hi there. You sent me: {'body'=>"can you say hi to me?", 'to'=>"greeter"}
38
+ => true
39
+
40
+
41
+ == TODO
42
+
43
+ Get docs working nicely (formatting, etc.) with RDoc.
44
+
45
+ Add tests to ensure that instance variables can be shared between initializers and helpers. For example:
46
+ class MyRouter < MessageRouter::Router
47
+ def initilaize(config)
48
+ @sender = config[:sender]
49
+ super
25
50
  end
26
-
27
- match /hi dude/ do
28
- "pleased to meet you"
51
+
52
+ match true do
53
+ send_something
29
54
  end
30
-
31
- match /hi (\w+)/ do |name|
32
- "how do you do #{name}"
55
+
56
+ def send_something
57
+ @sender.puts 'something'
33
58
  end
34
-
35
- match /hola (\w+)/, :from => 'bradgessler' do |name|
36
- "hello #{name} in spanish"
59
+ end
60
+ MyRouter.new(:sender => STDOUT).call({}) # prints out 'something' to standard out.
61
+
62
+ Pass Regexp captures on to the proc when there is a match. Examples:
63
+ match /some (cool|awesome) thing/ do |match|
64
+ puts "You thought the thing was #{match[1]}"
65
+ end
66
+ match 'some_attr' => /some (cool|awesome) thing/, 'body' => /(.*)/ do |matches|
67
+ puts "You thought that #{matches['body'][1]} was #{matches['some_attr'][1]}"
68
+ end
69
+ -- OR --
70
+ match /some (cool|awesome) thing/ do
71
+ puts "You thought the thing was #{env['message_router_match'][1]}"
72
+ end
73
+ match 'some_attr' => /some (cool|awesome) thing/, 'body' => /(.*)/ do
74
+ puts "You thought that #{env['message_router_matches']['body'][1]} was #{env['message_router_matches']['some_attr'][1]}"
75
+ end
76
+ -- OR -- (probably best because it is simplest)
77
+ match /some (cool|awesome) thing(.*)/ do |word, the_rest|
78
+ puts "You thought the thing was #{word}. But the rest is #{the_rest}"
79
+ end
80
+ match 'some_attr' => /some (cool|awesome) thing(.*)/, 'body' => /(.*)/ do |hash|
81
+ puts "You thought that #{hash['body']} was #{hash['some_attr'][0]}. But the rest is #{hash['some_attr'][1]}"
82
+ end
83
+ -- OR -- (if the note below about setting context/scope of annonymous functions is done)
84
+ match /some (cool|awesome) thing(.*)/ do |word, the_rest|
85
+ puts "You thought the thing was #{word}. But the rest is #{the_rest}"
86
+ end
87
+ match 'some_attr' => /some (cool|awesome) thing(.*)/, 'body' => /(.*)/ do |hash|
88
+ puts "You thought that #{hash['body']} was #{hash['some_attr'][0]}. But the rest is #{hash['some_attr'][1]}"
89
+ end
90
+
91
+ Improve specs to minimize use of global variables. The idea below about passing copies of the env hash around (instead of modifying it in place) might help here. I could have various bits of code being tested modify the env hash, and the final return value would be the env hash, which I could examine.
92
+
93
+ Consider making the String matcher more flexible. There could be options for:
94
+ * Exact match
95
+ * Case sensitivity
96
+ * Partial matches:
97
+ * starts with
98
+ * ends with
99
+ * contains
100
+
101
+ Recursion detection: It could be done by having a specific key in the message hash for parents. Before sending #call to a matcher's proc we could run something like "message['message_router_stack'] << self". Then we could check the size of this. The maximum number of levels would need to be configurable to allow some recursion.
102
+ * We could just rely on a stack overflow, but having recursion detection would make debugging easier.
103
+
104
+ Make helper methods defined (or included) in parent routers available in sub routers. It could be done with delegation, but that might get messy. It may be easier to not implement this and just require the user to use sub-classing to get the desired behavior.
105
+
106
+ Pass around copies of the env Hash instead of modifying the existing Hash in place.
107
+ * This _might_ help with multi-threading
108
+ * Perhaps a parent router wants to delegate to 2 sub-routers which are independent of each other. The current implementation has a shared env hash, so I couldn't use multi-threading, (though forking could work). I would have to trust the user to call #dup on at least one of the env hashes. With this new way, it is safe by default.
109
+ * Convention would be for the 'should_i' Procs to return a copy of the env hashes, either modified or not, depending on their needs.
110
+ * They would still return nil or false if they don't match.
111
+ * We could also require (by convention only) that sub-routers also return a copy of the env hash (if they succeed) so this (optionally modified) env hash can be used for further routing.
112
+ * This would give the original router access to both the modified env hash and the original env hash.
113
+
114
+ Find a way to allow user-defined 'do_this' procs/blocks to not have to return a true value to be considered to have matched. We still need a way to know if a sub-router matched or not. This _may_require that the code treat sub-routers and user-defined 'do_this' procs/blocks differently, which could get awkward.
115
+
116
+ Allow routers to accept an optional logger. Depending on the log level, print out info such as:
117
+ * When a matcher is registered
118
+ * Each time a matcher is evaluated, including what the return value was.
119
+ * Each time a 'do_this' block is evaluated, including what the return value was.
120
+ Each time we write to the log include the following (depending on the log level):
121
+ * The value of the env hash
122
+ * The name of the class (so we can tell which subclass we are in)
123
+ * Any instance variables set (so we know the config and if it changed)
124
+
125
+ Consider creating some sort of RouterRun class, each instance of which would encapsulate a call to MessageRouter::Router#call. This class would have all the helper methods as well as the #env helper method. This may help make this gem more threadsafe by keeping the shared state in the MessageRouter::Router objects immutable.
126
+
127
+ Consider having #call duplicate the router so that each run happens in its own instance. When MessageRouter::Router#call was called, call `self.dup.call(env, :no_dup)`. This would create a copy of the router for handling the message. It would be safe to use instance variables, except (maybe) deep-nested ones, but they could be handled by requiring the user to overwrite #dup.
128
+
129
+ Consider having a class called Run nested within the router's namespace. Instead of allowing users to define helper methods inside the router, they would be defined inside a subclass of MessageGateway::Router::Run. MessageGateway::Router#call might look something like:
130
+ def call(env)
131
+ Run.new(env, self).run
132
+ end
133
+ Run#run might look something like:
134
+ def run
135
+ router.rules.detect do |should_i, do_this|
136
+ should_i = if should_i.kind_of?(Proc)
137
+ self.instance_eval &should_i
138
+ else
139
+ should_i.call env
140
+ end
141
+
142
+ if should_i
143
+ do_this = if do_this.kind_of?(Proc)
144
+ self.instance_eval &do_this
145
+ else
146
+ do_this.call env
147
+ end
148
+
149
+ return do_this if do_this
150
+ end
37
151
  end
38
-
39
- private
40
- def all_caps_with_numbers
41
- message[:body] =~ /^[A-Z0-9\s]+$/
152
+ end
153
+ A user's router might look like this:
154
+ class MyRouter < MessageRouter::Router
155
+ match :hello? { say 'hi' }
156
+ class Run < MessageRouter::Router::Run
157
+ def hello?
158
+ env['body'] == 'hi'
159
+ end
160
+ def say(msg)
161
+ puts msg
162
+ end
42
163
  end
43
164
  end
165
+ This would make all instance variables inside the helpers safe and intuitive. I'd need to double check, but I think Ruby's constant lookup method would allow for inheritance to work fairly intuitively as long as the Run class and the router class both inherit from the same place. (I.e. if you inherit from MyBaseRouter, then your run class should also inherit from MyBaseRouter::Run.)
44
166
 
45
- And now some irb action.
46
-
47
- >> TwitterRouter.new(:body => 'hi dude').dispatch
48
- => {:body => "pleased to meet you"}
49
- >> TwitterRouter.new(:body => 'hi brad').dispatch
50
- => {:body => "how do you do brad"}
51
- >> TwitterRouter.new(:body => 'HI BRAD 90').dispatch
52
- => {:body => "STOP SHOUTING WITH NUMBERS!"}
53
- >> TwitterRouter.new(:body => 'HI BRAD').dispatch
54
- => {:body => "STOP SHOUTING WITHOUT NUMBERS!"}
55
167
 
56
168
  == License
57
169
 
58
- Copyright (c) 2010, Brad Gessler
170
+ Copyright (c) 2009-2012, Brad Gessler
59
171
 
60
172
  Permission is hereby granted, free of charge, to any person obtaining a copy
61
173
  of this software and associated documentation files (the "Software"), to deal
@@ -73,4 +185,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
73
185
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
74
186
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
75
187
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
76
- THE SOFTWARE.
188
+ THE SOFTWARE.
@@ -1,73 +1,5 @@
1
1
  require "message_router/version"
2
2
 
3
3
  class MessageRouter
4
-
5
- autoload :Context, 'message_router/context'
6
- autoload :Matcher, 'message_router/matcher'
7
- autoload :Mount, 'message_router/mount'
8
-
9
- class << self
10
- def match(*args, &block)
11
- route Matcher.new(*args, &block)
12
- end
13
-
14
- def context(proc, &block)
15
- route Context.new(proc, &block)
16
- end
17
-
18
- def mount(mounted_router_klass)
19
- route Mount.new(mounted_router_klass)
20
- end
21
-
22
- def routes
23
- @routes ||= []
24
- end
25
-
26
- def route(proc)
27
- routes.push proc
28
- end
29
-
30
- def dispatch(*args)
31
- new(*args).dispatch
32
- end
33
- end
34
-
35
- attr_accessor :message, :halted_value
36
-
37
- def initialize(*args)
38
- @message = normalize_arguments(*args)
39
- end
40
-
41
- def halt(val=nil, opts={})
42
- @halted = true
43
- @halted_value = normalize_arguments(val, opts)
44
- end
45
-
46
- def halted?
47
- !!@halted
48
- end
49
-
50
- # Iterate through all of the matchers, find the first one, and call the block on it.
51
- def dispatch
52
- self.class.routes.each do |route|
53
- # Break out of the loop if a match is found
54
- if match = route.call(self)
55
- return match
56
- elsif halted?
57
- return halted_value
58
- end
59
- end
60
- return nil # If nothing is matched, we get here and we should return a nil
61
- end
62
-
63
- def default_key
64
- :body
65
- end
66
-
67
- private
68
- # Make our router accept the first argument as the default message key, then optional keys last.
69
- def normalize_arguments(message=nil, opts={})
70
- message = opts.merge(:body => message) unless message.is_a? Hash and opts.empty?
71
- message
72
- end
73
- end
4
+ autoload :Router, 'message_router/router'
5
+ end
@@ -0,0 +1,320 @@
1
+ class MessageRouter
2
+ # To define a router, subclass MessageRouter::Router, then call #match
3
+ # inside the class definition.
4
+ # An example:
5
+ # class MyApp::Router::Application < MessageRouter::Router
6
+ # # Share helpers between routers by including modules
7
+ # include MyApp::Router::MyHelper
8
+ #
9
+ # prerequisite :db_connected?
10
+ #
11
+ # match SomeOtherRouter.new
12
+ # # `mount` is an alias of `match`
13
+ # mount AnotherRouter.new
14
+ #
15
+ # match(lamba { env['from'].nil? }) do
16
+ # Logger.error "Can't reply when when don't know who a message is from: #{env.inspect}"
17
+ # end
18
+ #
19
+ # match 'ping' do
20
+ # PingCounter.increment!
21
+ # send_reply 'pong', env
22
+ # end
23
+ #
24
+ # match /\Ahelp/i do
25
+ # SupportQueue.contact_asap(env['from'])
26
+ # send_reply 'Looks like you need some help. Hold tight someone will call you soon.', env
27
+ # end
28
+ #
29
+ # # StopRouter would have been defined just like this router.
30
+ # match /\Astop/i, MyApp::Router::StopRouter
31
+ #
32
+ # match 'to' => /(12345|54321)/ do
33
+ # Logger.warn "Use of deprecated short code: #{msg.inspect}"
34
+ # send_reply "Sorry, you are trying to use a deprecated short code. Please try again.", env
35
+ # end
36
+ #
37
+ # match :user_name => PriorityUsernameRouter.new
38
+ # match :user_name, OldStyleUsernameRouter.new
39
+ # match :user_name do
40
+ # send_reply "I found you! Your name is #{user_name}.", env
41
+ # end
42
+ #
43
+ # match %w(stop end quit), StopRouter.new
44
+ #
45
+ # # Array elements don't need to be the same type
46
+ # match [
47
+ # :user_is_a_tester,
48
+ # {'to' => %w(12345 54321)},
49
+ # {'RAILS_ENV' => 'test'},
50
+ # 'test'
51
+ # ], TestRouter.new
52
+ #
53
+ # # Works inside a Hash too
54
+ # match 'from' => ['12345', '54321', /111\d\d/] do
55
+ # puts "'#{env['from']}' is a funny looking short code"
56
+ # end
57
+ #
58
+ # match true do
59
+ # UserMessage.create! env
60
+ # send_reply "Sorry we couldn't figure out how to handle your message. We have recorded it and someone will get back to you soon.", env
61
+ # end
62
+ #
63
+ #
64
+ # def send_reply(body, env)
65
+ # OutgoingMessage.deliver!(:body => body, :to => env['from'], :from => env['to'])
66
+ # end
67
+ #
68
+ # def user_name(env)
69
+ # env['user_name'] ||= User.find(env['from'])
70
+ # end
71
+ #
72
+ # def db_connected?
73
+ # Database.connected?
74
+ # end
75
+ # end
76
+ #
77
+ # router = MyApp::Router::Application.new
78
+ # router.call({}) # Logs an error about not knowing who the message is from
79
+ # router.call({'from' => 'mr-smith', 'body' => 'ping'}) # Sends a 'pong' reply
80
+ # router.call({'from' => 'mr-smith', 'to' => 12345}) # Sends a deprecation warning reply
81
+ class Router
82
+
83
+ class << self
84
+ # The 1st argument to a matcher can be:
85
+ # * true, false, or nil
86
+ # * String or Regexp, which match against env['body']. Strings match against
87
+ # the 1st word.
88
+ # * Array - Elements can be Strings or Regexps. They are matched against
89
+ # 'body'. Matches if any element is matches.
90
+ # * Hash - Keys are expected to be a subset of the env's keys. The
91
+ # values are String, Regexp, or Array to be match again the corresponding
92
+ # value in the env Hash. True if there is a match for all keys.
93
+ # * Symbol - Calls a helper method of the same name. If the helper can take
94
+ # an argument, the env will be passed to it. The return value of the
95
+ # helper method determines if the matcher matches.
96
+ # * Anything that responds to #call - It is passed the env as the only
97
+ # arugment. The return value determines if the matcher matches.
98
+ # Because Routers are trigged by the method #call, one _could_ use a Router
99
+ # as the 1st argument to a matcher. However, it would actually run that
100
+ # Router's code, which is not intuitive, and therefore not recommonded.
101
+ # If the 1st argument to a matcher resolves to a true value, then the 2nd
102
+ # argument is sent `#call(env)`. If that also returns a true value,
103
+ # then the matcher has "matched" and the router stops. However, if the 2nd
104
+ # argument returns false, then the router will continue running. This
105
+ # allows us to mount sub-routers and continue trying other rules if those
106
+ # subrouters fail to match something.
107
+ # The 2nd argument to #match can also be specified with a block.
108
+ # If the 1st argument is skipped, then it is assumed to be true. This is
109
+ # useful for passing a message to a sub-router, which will return nil if
110
+ # it doesn't match. For example:
111
+ # match MyOtherRouter.new
112
+ # is a short-hand for:
113
+ # match true, MyOtherRouter.new
114
+ # It is important to keep in mind that blocks, procs, and lambdas, whether
115
+ # they are the 1st or 2nd argument, will be run in the scope of the router,
116
+ # just like the methods referenced by Symbols. That means that they have
117
+ # access to all the helper methods. However, it also means they have the
118
+ # ability to edit/add instance variables on the router; NEVER DO THIS. If
119
+ # you want to use an instance variable inside a helper, block, proc, or
120
+ # lambda, you MUST use the env hash instance. Examples:
121
+ # # BAD
122
+ # match :my_helper do
123
+ # @cached_user ||= User.find_by_id(@user_id)
124
+ # end
125
+ # def find_user
126
+ # @id ||= User.get_id_from_guid(env['guid'])
127
+ # end
128
+ #
129
+ # # GOOD
130
+ # match :my_helper do
131
+ # env['cached_user'] ||= User.find_by_id(env['user_id'])
132
+ # end
133
+ # def find_user
134
+ # env['id'] ||= User.get_id_from_guid(env['guid'])
135
+ # end
136
+ # If you do not follow this requirement, then when subsequent keywords are
137
+ # routed, they will see the instance variables from the previous message.
138
+ # In the case of the above example, every subsequent message will have
139
+ # @cached_user set the the user for the 1st message.
140
+ def match *args, &block
141
+ args << block if block
142
+ case args.size
143
+ when 0
144
+ raise ArgumentError, "You must provide either a block or an argument which responds to call."
145
+ when 1
146
+ if args[0].respond_to?(:call)
147
+ should_i = true
148
+ do_this = args[0]
149
+ elsif args[0].kind_of?(Hash) && args[0].values.size == 1 && args[0].values[0].respond_to?(:call)
150
+ # Syntactical suger to make:
151
+ # match :cool? => OnlyForCoolPeopleRouter.new
152
+ # work just like:
153
+ # match :cool?, OnlyForCoolPeopleRouter.new
154
+ should_i = args[0].keys[0]
155
+ do_this = args[0].values[0]
156
+ else
157
+ raise ArgumentError, "You must provide either a block or a 2nd argument which responds to call."
158
+ end
159
+ when 2
160
+ should_i, do_this = args
161
+ raise ArgumentError, "The 2nd argument must respond to call." unless do_this.respond_to?(:call)
162
+ else
163
+ raise ArgumentError, "Too many arguments. Note: you may not provide a block when a 2nd argument has been provided."
164
+ end
165
+
166
+ # Save the arguments for later.
167
+ rules << [should_i, do_this]
168
+ end
169
+ alias :mount :match
170
+
171
+ # Defines a prerequisite for this router. Prerequisites are like rules,
172
+ # except that if any of them don't match, the rest of the router is
173
+ # skipped.
174
+ # Anything that can be the 1st argument to `match` can be passed as an
175
+ # argument to `prerequisite`.
176
+ def prerequisite(arg=nil, &block)
177
+ arg ||= block if block
178
+ prerequisites << arg
179
+ end
180
+
181
+ # The rules are defined at the class level. But any helper methods
182
+ # referenced by Symbols are defined/executed at the instance level.
183
+ def rules
184
+ @rules ||= []
185
+ end
186
+
187
+ def prerequisites
188
+ @prerequisites ||= []
189
+ end
190
+ end
191
+
192
+
193
+ # This method initializes all the rules stored at the class level. When you
194
+ # create your subclass, if you want to add your own initializer, it is very
195
+ # important to call `super` or none of your rules will be matched.
196
+ def initialize
197
+ @rules = []
198
+ # Actually create the rules so that the procs we create are in the
199
+ # context of an instance of this object. This is most important when the
200
+ # rule is based on a symbol. We need that symbol to resolve to an
201
+ # instance method; however, instance methods are not available until
202
+ # after an instance is created.
203
+ self.class.rules.each {|rule| match *rule }
204
+
205
+ @prerequisites = []
206
+ self.class.prerequisites.each {|prerequisite| @prerequisites << normalize_match_params(prerequisite) }
207
+ end
208
+
209
+ # Kicks off the router. 'env' is a Hash. The keys are up to the user;
210
+ # however, the default key (used when a matcher is just a String or Regexp)
211
+ # is 'body'. If you don't specify this key, then String and Regexp matchers
212
+ # will always be false.
213
+ # Returns nil if no rules match
214
+ # Returns true if a rule matches
215
+ # A rule "matches" if both its procs return true. For example:
216
+ # match(true) { true }
217
+ # matches. However:
218
+ # match(true) { false }
219
+ # does not count as a match. This allows us to mount sub-routers and
220
+ # continue trying other rules if those subrouters fail to match something.
221
+ # However, this does mean you need to be careful when writing the 2nd
222
+ # argument to #match. If you return nil or false, the router will keep
223
+ # looking for another match.
224
+ def call(env)
225
+ # I'm pretty sure this is NOT thread safe. Having two threads use the
226
+ # same router at the same time will almost certainly give you VERY weird
227
+ # and incorrect results. We may want to introduce a RouterRun object to
228
+ # encapsulate one invocation of this #call method.
229
+ @env = env
230
+
231
+ # All prerequisites must return true in order to continue.
232
+ return false unless @prerequisites.all? do |should_i|
233
+ if should_i.kind_of?(Proc)
234
+ self.instance_eval &should_i
235
+ else
236
+ should_i.call @env
237
+ end
238
+ end
239
+
240
+ @rules.detect do |should_i, do_this|
241
+ should_i = if should_i.kind_of?(Proc)
242
+ self.instance_eval &should_i
243
+ else
244
+ should_i.call @env
245
+ end
246
+
247
+ if should_i
248
+ do_this = if do_this.kind_of?(Proc)
249
+ self.instance_eval &do_this
250
+ else
251
+ do_this.call @env
252
+ end
253
+
254
+ return true if do_this
255
+ end
256
+ end
257
+ ensure
258
+ @env = nil
259
+ end
260
+
261
+
262
+ private
263
+ def env; @env; end
264
+
265
+ def match(should_i, do_this)
266
+ @rules << [normalize_match_params(should_i), do_this]
267
+ end
268
+
269
+ def normalize_match_params(should_i=nil, &block)
270
+ should_i ||= block if block
271
+
272
+ case should_i
273
+ when Regexp, String
274
+ # TODO: Consider making this default attribute configurable.
275
+ normalize_match_params 'body' => should_i
276
+
277
+ when TrueClass, FalseClass, NilClass
278
+ Proc.new { should_i }
279
+
280
+ when Symbol
281
+ Proc.new do
282
+ self.send should_i
283
+ end
284
+
285
+ when Array
286
+ should_i = should_i.map {|x| normalize_match_params x}
287
+ Proc.new do
288
+ should_i.any? { |x| x.call env }
289
+ end
290
+
291
+ when Hash
292
+ Proc.new do
293
+ should_i.all? do |key, val|
294
+ attr_matches? env[key], val
295
+ end
296
+ end
297
+
298
+ else
299
+ # Assume it already responds to #call.
300
+ should_i
301
+ end
302
+
303
+ end
304
+
305
+ def attr_matches?(attr, val)
306
+ case val
307
+ when String
308
+ attr =~ /\A#{val}\b/i # Match 1st word
309
+ when Regexp
310
+ attr =~ val
311
+ when Array
312
+ val.any? do |x|
313
+ attr_matches? attr, x
314
+ end
315
+ else
316
+ raise "Unexpected value '#{val.inspect}'. Should be String, Regexp, or Array of Strings and Regexps."
317
+ end
318
+ end
319
+ end
320
+ end