Class: MessageRouter::Router
- Inherits:
-
Object
- Object
- MessageRouter::Router
- Defined in:
- lib/message_router/router.rb
Overview
To define a router, subclass MessageRouter::Router, then call #match inside the class definition. An example:
class MyApp::Router::Application < MessageRouter::Router
# Share helpers between routers by including modules
include MyApp::Router::MyHelper
prerequisite :db_connected?
match SomeOtherRouter
# `mount` is an alias of `match`
mount AnotherRouter
match(lambda { env['from'].nil? }) do
Logger.error "Can't reply when when don't know who a message is from: #{env.inspect}"
end
# Matches if the first word of env['body'] is PING (case insensitive).
# Overwrite #default_attribute in your router to match against a
# different value.
match 'ping' do
PingCounter.increment!
send_reply 'pong', env
end
# Matches if env['body'] matches the given Regexp.
# Overwrite #default_attribute in your router to match against a
# different value.
match /\Ahelp/i do
SupportQueue.contact_asap(env['from'])
send_reply 'Looks like you need some help. Hold tight someone will call you soon.', env
end
# StopRouter would have been defined just like this router.
match /\Astop/i, MyApp::Router::StopRouter
match 'to' => /(12345|54321)/ do
Logger.warn "Use of deprecated short code: #{msg.inspect}"
send_reply "Sorry, you are trying to use a deprecated short code. Please try again.", env
end
match :user_name => PriorityUsernameRouter
match :user_name, OldStyleUsernameRouter
match :user_name do
send_reply "I found you! Your name is #{user_name}.", env
end
match %w(stop end quit), StopRouter
# Array elements don't need to be the same type
match [
:user_is_a_tester,
{'to' => %w(12345 54321)},
{'RAILS_ENV' => 'test'},
'test'
], TestRouter
# Works inside a Hash too
match 'from' => ['12345', '54321', /111\d\d/] do
puts "'#{env['from']}' is a funny looking short code"
end
match true do
UserMessage.create! env
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
end
def send_reply(body, env)
OutgoingMessage.deliver!(:body => body, :to => env['from'], :from => env['to'])
end
def user_name(env)
env['user_name'] ||= User.find(env['from'])
end
def db_connected?
Database.connected?
end
end
router = MyApp::Router::Application
router.call({}) # Logs an error about not knowing who the message is from
router.call({'from' => 'mr-smith', 'body' => 'ping'}) # Sends a 'pong' reply
router.call({'from' => 'mr-smith', 'to' => 12345}) # Sends a deprecation warning reply
Class Method Summary collapse
-
.call(env) ⇒ Object
Kicks off the router.
-
.match(*args, &block) ⇒ Object
The 1st argument to a matcher can be: * true, false, or nil * String or Regexp, which match against env.
-
.mount ⇒ Object
The 1st argument to a matcher can be: * true, false, or nil * String or Regexp, which match against env.
-
.prerequisite(arg = nil, &block) ⇒ Object
Defines a prerequisite for this router.
- .prerequisites ⇒ Object
-
.rules ⇒ Object
The rules are defined at the class level.
Instance Method Summary collapse
- #env ⇒ Object
-
#initialize(env) ⇒ Router
constructor
This method initializes all the rules stored at the class level.
- #matched ⇒ Object
- #matched? ⇒ Boolean
- #not_matched ⇒ Object
-
#run ⇒ Object
:nodoc:.
Constructor Details
#initialize(env) ⇒ Router
This method initializes all the rules stored at the class level. When you create your subclass, if you want to add your own initializer, it is very important to call ‘super` or none of your rules will be matched.
221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 |
# File 'lib/message_router/router.rb', line 221 def initialize(env) #:nodoc: @env = env.dup # a parent router may be assuming a successful match # but this subrouter may not, so we explicitly set it to not matched # on creation not_matched @rules = [] # Actually create the rules so that the procs we create are in the # context of an instance of this object. This is most important when the # rule is based on a symbol. We need that symbol to resolve to an # instance method; however, instance methods are not available until # after an instance is created. self.class.rules.each {|rule| match *rule } @prerequisites = [] self.class.prerequisites.each do |prerequisite| @prerequisites << normalize_match_params(prerequisite) end end |
Class Method Details
.call(env) ⇒ Object
Kicks off the router. ‘env’ is a Hash. The keys are up to the user; however, the default key (used when a matcher is just a String or Regexp) is ‘body’. If you don’t specify this key, then String and Regexp matchers will always be false. Returns a new instance of this class, that gets run before being returned A rule “matches” if its condition return true and the action does not explicitly call not_matched. For example:
match(true) { }
matches. However:
match(true) { not_matched }
does not count as a match. This allows us to mount sub-routers and continue trying other rules if those subrouters fail to match something.
212 213 214 |
# File 'lib/message_router/router.rb', line 212 def call(env) new(env).run end |
.match(*args, &block) ⇒ Object
The 1st argument to a matcher can be:
-
true, false, or nil
-
String or Regexp, which match against env. Strings match against the 1st word.
-
Array - Elements can be Strings or Regexps. They are matched against ‘body’. Matches if any element is matches.
-
Hash - Keys are expected to be a subset of the env’s keys. The values are String, Regexp, or Array to be match again the corresponding value in the env Hash. True if there is a match for all keys.
-
Symbol - Calls a helper method of the same name. If the helper can take an argument, the env will be passed to it. The return value of the helper method determines if the matcher matches.
-
Anything that responds to #call - It is passed the env as the only arugment. The return value determines if the matcher matches.
Because Routers are trigged by the method #call, one could use a Router as the 1st argument to a matcher. However, it would actually run that Router’s code, which is not intuitive, and therefore not recommonded. If the 1st argument to a matcher resolves to a true value, then the 2nd argument is sent ‘#call(env)`. If that also returns a true value, then the matcher has “matched” and the router stops. However, if the 2nd argument returns false, then the router will continue running. This allows us to mount sub-routers and continue trying other rules if those subrouters fail to match something. The 2nd argument to #match can also be specified with a block. If the 1st argument is skipped, then it is assumed to be true. This is useful for passing a message to a sub-router, which will return nil if it doesn’t match. For example:
match MyOtherRouter.new
is a short-hand for:
match true, MyOtherRouter.new
It is important to keep in mind that blocks, procs, and lambdas, whether they are the 1st or 2nd argument, will be run in the scope of the router, just like the methods referenced by Symbols. That means that they have access to all the helper methods. However, it also means they have the ability to edit/add instance variables on the router; NEVER DO THIS. If you want to use an instance variable inside a helper, block, proc, or lambda, you MUST use the env hash instance. Examples:
# BAD
match :my_helper do
@cached_user ||= User.find_by_id(@user_id)
end
def find_user
@id ||= User.get_id_from_guid(env['guid'])
end
# GOOD
match :my_helper do
env['cached_user'] ||= User.find_by_id(env['user_id'])
end
def find_user
env['id'] ||= User.get_id_from_guid(env['guid'])
end
If you do not follow this requirement, then when subsequent keywords are routed, they will see the instance variables from the previous message. In the case of the above example, every subsequent message will have
146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 |
# File 'lib/message_router/router.rb', line 146 def match *args, &block args << block if block case args.size when 0 raise ArgumentError, "You must provide either a block or an argument which responds to call." when 1 if args[0].respond_to?(:env) condition = true action = args[0] elsif args[0].respond_to?(:call) condition = true action = args[0] elsif args[0].kind_of?(Hash) && args[0].values.size == 1 && args[0].values[0].respond_to?(:call) # Syntactical suger to make: # match :cool? => OnlyForCoolPeopleRouter # work just like: # match :cool?, OnlyForCoolPeopleRouter condition = args[0].keys[0] action = args[0].values[0] else raise ArgumentError, "You must provide either a block or a 2nd argument which responds to call." end when 2 condition, action = args raise ArgumentError, "The 2nd argument must respond to call." unless action.respond_to?(:call) else raise ArgumentError, "Too many arguments. Note: you may not provide a block when a 2nd argument has been provided." end # Save the arguments for later. rules << [condition, action] end |
.mount ⇒ Object
The 1st argument to a matcher can be:
-
true, false, or nil
-
String or Regexp, which match against env. Strings match against the 1st word.
-
Array - Elements can be Strings or Regexps. They are matched against ‘body’. Matches if any element is matches.
-
Hash - Keys are expected to be a subset of the env’s keys. The values are String, Regexp, or Array to be match again the corresponding value in the env Hash. True if there is a match for all keys.
-
Symbol - Calls a helper method of the same name. If the helper can take an argument, the env will be passed to it. The return value of the helper method determines if the matcher matches.
-
Anything that responds to #call - It is passed the env as the only arugment. The return value determines if the matcher matches.
Because Routers are trigged by the method #call, one could use a Router as the 1st argument to a matcher. However, it would actually run that Router’s code, which is not intuitive, and therefore not recommonded. If the 1st argument to a matcher resolves to a true value, then the 2nd argument is sent ‘#call(env)`. If that also returns a true value, then the matcher has “matched” and the router stops. However, if the 2nd argument returns false, then the router will continue running. This allows us to mount sub-routers and continue trying other rules if those subrouters fail to match something. The 2nd argument to #match can also be specified with a block. If the 1st argument is skipped, then it is assumed to be true. This is useful for passing a message to a sub-router, which will return nil if it doesn’t match. For example:
match MyOtherRouter.new
is a short-hand for:
match true, MyOtherRouter.new
It is important to keep in mind that blocks, procs, and lambdas, whether they are the 1st or 2nd argument, will be run in the scope of the router, just like the methods referenced by Symbols. That means that they have access to all the helper methods. However, it also means they have the ability to edit/add instance variables on the router; NEVER DO THIS. If you want to use an instance variable inside a helper, block, proc, or lambda, you MUST use the env hash instance. Examples:
# BAD
match :my_helper do
@cached_user ||= User.find_by_id(@user_id)
end
def find_user
@id ||= User.get_id_from_guid(env['guid'])
end
# GOOD
match :my_helper do
env['cached_user'] ||= User.find_by_id(env['user_id'])
end
def find_user
env['id'] ||= User.get_id_from_guid(env['guid'])
end
If you do not follow this requirement, then when subsequent keywords are routed, they will see the instance variables from the previous message. In the case of the above example, every subsequent message will have
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 |
# File 'lib/message_router/router.rb', line 178 def match *args, &block args << block if block case args.size when 0 raise ArgumentError, "You must provide either a block or an argument which responds to call." when 1 if args[0].respond_to?(:env) condition = true action = args[0] elsif args[0].respond_to?(:call) condition = true action = args[0] elsif args[0].kind_of?(Hash) && args[0].values.size == 1 && args[0].values[0].respond_to?(:call) # Syntactical suger to make: # match :cool? => OnlyForCoolPeopleRouter # work just like: # match :cool?, OnlyForCoolPeopleRouter condition = args[0].keys[0] action = args[0].values[0] else raise ArgumentError, "You must provide either a block or a 2nd argument which responds to call." end when 2 condition, action = args raise ArgumentError, "The 2nd argument must respond to call." unless action.respond_to?(:call) else raise ArgumentError, "Too many arguments. Note: you may not provide a block when a 2nd argument has been provided." end # Save the arguments for later. rules << [condition, action] end |
.prerequisite(arg = nil, &block) ⇒ Object
Defines a prerequisite for this router. Prerequisites are like rules, except that if any of them don’t match, the rest of the router is skipped. Anything that can be the 1st argument to ‘match` can be passed as an argument to `prerequisite`.
185 186 187 188 |
# File 'lib/message_router/router.rb', line 185 def prerequisite(arg=nil, &block) arg ||= block if block prerequisites << arg end |
.prerequisites ⇒ Object
196 197 198 |
# File 'lib/message_router/router.rb', line 196 def prerequisites @prerequisites ||= [] end |
.rules ⇒ Object
The rules are defined at the class level. But any helper methods referenced by Symbols are defined/executed at the instance level.
192 193 194 |
# File 'lib/message_router/router.rb', line 192 def rules @rules ||= [] end |
Instance Method Details
#env ⇒ Object
270 |
# File 'lib/message_router/router.rb', line 270 def env; @env; end |
#matched ⇒ Object
262 263 264 |
# File 'lib/message_router/router.rb', line 262 def matched env['_matched'] = true end |
#matched? ⇒ Boolean
265 266 267 |
# File 'lib/message_router/router.rb', line 265 def matched? !!env['_matched'] end |
#not_matched ⇒ Object
259 260 261 |
# File 'lib/message_router/router.rb', line 259 def not_matched env['_matched'] = false end |
#run ⇒ Object
:nodoc:
241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 |
# File 'lib/message_router/router.rb', line 241 def run #:nodoc: # All prerequisites must return true in order to continue. return self unless @prerequisites.all? do |condition| self.instance_eval &condition end @rules.detect do |condition, action| if self.instance_eval &condition matched r = self.instance_eval &action @env = r.respond_to?(:env) ? r.env : r return self if matched? end end self # always return router instance end |