message_router 0.0.2 → 0.1.1

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.
@@ -1,3 +1,3 @@
1
1
  class MessageRouter
2
- VERSION = "0.0.2"
3
- end
2
+ VERSION = "0.1.1"
3
+ end
@@ -5,8 +5,8 @@ require "message_router/version"
5
5
  Gem::Specification.new do |s|
6
6
  s.name = "message_router"
7
7
  s.version = MessageRouter::VERSION
8
- s.authors = ["Brad Gessler"]
9
- s.email = ["brad@bradgessler.com"]
8
+ s.authors = ["Brad Gessler", "Paul Cortens"]
9
+ s.email = ["brad@bradgessler.com", "paul@thoughtless.ca"]
10
10
  s.homepage = ""
11
11
  s.summary = %q{Route messages}
12
12
  s.description = %q{a DSL for routing SMS, Twitter, and other short message formats.}
@@ -17,4 +17,4 @@ Gem::Specification.new do |s|
17
17
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
18
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
19
  s.require_paths = ["lib"]
20
- end
20
+ end
@@ -1,155 +1,464 @@
1
1
  require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
2
 
3
- describe MessageRouter::Matcher do
4
- context "regexp expressions" do
5
- it "should return captures" do
6
- match = MessageRouter::Matcher.new({:body => /(\w+) (\w+) (\w+)/}).match({:body => 'hi there dude'})
7
- match[:body].should eql(%w(hi there dude))
8
- end
9
- end
10
-
11
- context "string expressions" do
12
- it "should return capture" do
13
- match = MessageRouter::Matcher.new({:body => 'testing'}).match({:body => 'testing'})
14
- match[:body].should eql(['testing'])
15
- end
16
- end
17
-
18
- it "should return captures if all matchers have captures" do
19
- match = MessageRouter::Matcher.new({:body => 'testing', :carrier => 'att'}).match({:body => 'testing', :carrier => 'att'})
20
- match.should_not be_nil
21
- end
22
-
23
- it "should not return captures if some matchers have captures" do
24
- match = MessageRouter::Matcher.new({:body => 'testing', :carrier => 'att'}).match({:body => 'testing', :carrier => 'verizon'})
25
- match.should be_nil
26
- end
27
-
28
- it "should not return captures of params are missing" do
29
- match = MessageRouter::Matcher.new({:body => 'testing', :carrier => 'att'}).match({:body => 'testing'})
30
- end
31
-
32
- it "should default matcher param to :body" do
33
- MessageRouter::Matcher.default_key.should eql(:body)
34
- end
35
- end
3
+ # TODO: Maybe move this into a sub-directory
4
+ describe MessageRouter::Router do
36
5
 
37
- describe MessageRouter do
38
- class CrazyTimesRouter < MessageRouter
39
- match /^crazy$/ do
40
- halt "factory blow out sales are awesome"
41
- end
42
- end
6
+ describe ".build" do
7
+
8
+ describe 'defining matchers' do
9
+ describe '1st argument' do
10
+ before do
11
+ # For use in confirming whether or not a proc was called.
12
+ $thing_to_match = $did_it_run = nil
13
+ end
14
+
15
+ let :env do
16
+ {
17
+ 'body' => 'hello world',
18
+ 'from' => '15554443333',
19
+ 'to' => '12345'
20
+ }
21
+ end
22
+
23
+ # This needs to be a method (and not memoized by #let) so that
24
+ # $thing_to_match can change within a test.
25
+ def router
26
+ Class.new MessageRouter::Router do
27
+ match($thing_to_match) { $did_it_run = true }
28
+
29
+ # Using these methods also proves that the message is optionally
30
+ # passed to helper methods.
31
+ def always_true
32
+ env['body'] == 'hello world'
33
+ end
34
+ def always_false
35
+ false
36
+ end
37
+ end.new
38
+ end
39
+
40
+ let :the_test do
41
+ Proc.new do |opts|
42
+ $thing_to_match = opts[:true]
43
+ router.call(env)
44
+ $did_it_run.should == true
45
+ $did_it_run = nil # reset for next time
46
+
47
+ $thing_to_match = opts[:false]
48
+ router.call(env)
49
+ $did_it_run.should == nil
50
+ $did_it_run = nil # reset for next time
51
+ end
52
+ end
53
+
54
+ it 'accepts a boolean' do
55
+ the_test.call :true => true, :false => false
56
+ end
57
+
58
+ it 'accepts a nil' do
59
+ # True is just here as a placeholder
60
+ the_test.call :true => true, :false => nil
61
+ end
62
+
63
+ it 'accepts a proc which is passed the env' do
64
+ the_test.call(
65
+ :true => Proc.new { env['to'] == '12345'},
66
+ :false => Proc.new { env['to'] == '54321'}
67
+ )
68
+ end
69
+
70
+ it 'accepts a regex to match against the message body' do
71
+ the_test.call :true => /hello/, :false => /bye bye/
72
+ end
73
+
74
+ it 'accepts a string to match against the 1st word in the message body' do
75
+ the_test.call :true => 'hello', :false => 'hell'
76
+ end
77
+
78
+ it 'accepts a symbol which is a method name' do
79
+ the_test.call :true => :always_true, :false => :always_false
80
+ end
43
81
 
44
- # Test case router
45
- class TwitterRouter < MessageRouter
46
- context :all_caps_with_numbers do |funny_word, high_def_resolution|
47
- match /FUNNYWORD/i do
48
- halt "#{funny_word}-#{high_def_resolution}"
82
+ it 'accepts an Array' do
83
+ the_test.call :true => %w(only one of these needs to be the word hello), :false => %w(none of these match)
84
+ end
85
+
86
+ describe 'matching an Array' do
87
+ it "doesn't run the 'do_this' block multiple times if there are multiple matches" do
88
+ $run_count = 0
89
+ router = Class.new(MessageRouter::Router) do
90
+ match [true, true] do
91
+ $run_count += 1
92
+ nil # Return nil to ensure this matcher failed.
93
+ end
94
+ end.new
95
+
96
+ router.call({})
97
+ $run_count.should == 1
98
+ end
99
+
100
+ it "returns nil if the 'do_this' block returns nil" do
101
+ $run_count = 0
102
+ router = Class.new(MessageRouter::Router) do
103
+ match [true, true] do
104
+ $run_count += 1
105
+ nil # Return nil to ensure this matcher failed.
106
+ end
107
+ end.new
108
+
109
+ router.call({}).should == nil
110
+ end
111
+
112
+ end
113
+
114
+ describe 'matching a hash' do
115
+ it 'accepts a string to match against the hash key' do
116
+ the_test.call(
117
+ :true => {
118
+ 'from' => '15554443333',
119
+ 'to' => '12345'
120
+ },
121
+ :false => {
122
+ 'from' => 'something-else',
123
+ 'to' => '12345'
124
+ }
125
+ )
126
+ end
127
+ it 'accepts a regex to match against the hash key' do
128
+ the_test.call(
129
+ :true => {
130
+ 'from' => /\A1555\d{7}\Z/,
131
+ 'to' => /\A\d{5}\Z/
132
+ },
133
+ :false => {
134
+ 'from' => /\A1555\d{7}\Z/,
135
+ 'to' => /i don't match/
136
+ }
137
+ )
138
+ end
139
+ it 'accepts an Array to match against the hash key' do
140
+ the_test.call(
141
+ :true => {
142
+ 'from' => [/i don't match/, 'neither do i', 'but this last one does', /\A1555\d{7}\Z/],
143
+ 'to' => [/i don't match/, 'neither do i', 'but this last one does', /\A\d{5}\Z/]
144
+ },
145
+ :false => {
146
+ 'from' => [/\A1555\d{7}\Z/, 'that last one did not match'],
147
+ 'to' => [/i don't match/, 'neither do i']
148
+ }
149
+ )
150
+ end
151
+
152
+ it 'accepts keys that are missing (but is always false)' do
153
+ $thing_to_match = {'i dont exist' => /.*/}
154
+ router.call(env)
155
+ $did_it_run.should == nil
156
+ $did_it_run = nil # reset for next time
157
+ end
158
+ end
49
159
  end
50
160
 
51
- # All caps without numbers... but in a proc
52
- context Proc.new{|r| r.message[:body] =~ /^[A-Z\s]+$/ } do
53
- match /.+/ do
54
- halt "STOP SHOUTING WITHOUT NUMBERS!"
161
+ describe '2nd argument' do
162
+ it 'accepts a Proc' do
163
+ env = {}
164
+ router = Class.new MessageRouter::Router do
165
+ match(true, Proc.new { env['did_it_run'] = true })
166
+ end.new
167
+ router.call env
168
+ env['did_it_run'].should be_true
169
+ end
170
+
171
+ it 'accepts a block' do
172
+ env = {}
173
+ router = Class.new MessageRouter::Router do
174
+ match(true) { env['did_it_run'] = true }
175
+ end.new
176
+ router.call env
177
+ env['did_it_run'].should be_true
178
+ end
179
+
180
+ it 'raises an execption when both a Proc and a block are given' do
181
+ lambda {
182
+ router = Class.new MessageRouter::Router do
183
+ match(true, Proc.new { env['did_it_run'] = true }) { env['did_it_run'] = true }
184
+ end.new
185
+ }.should raise_error(ArgumentError)
186
+ end
187
+
188
+ it 'raises an execption when neither a Proc nor a block are given' do
189
+ lambda {
190
+ router = Class.new MessageRouter::Router do
191
+ match true
192
+ end.new
193
+ }.should raise_error(ArgumentError)
55
194
  end
56
195
  end
57
196
 
58
- match /.+/ do
59
- halt "STOP SHOUTING WITH NUMBERS!"
197
+ it 'defaults the 1st argument to true if only a block is given' do
198
+ env = {}
199
+ router = Class.new MessageRouter::Router do
200
+ match { env['did_it_run'] = true }
201
+ end.new
202
+ router.call env
203
+ env['did_it_run'].should be_true
60
204
  end
61
- end
62
205
 
63
- mount CrazyTimesRouter
206
+ it 'defaults the 1st argument to true if only a Proc is given' do
207
+ env = {}
208
+ router = Class.new MessageRouter::Router do
209
+ match(Proc.new { env['did_it_run'] = true })
210
+ end.new
211
+ router.call env
212
+ env['did_it_run'].should be_true
213
+ end
64
214
 
65
- match /hi dude/ do
66
- halt "pleased to meet you"
67
- end
215
+ it 'accepts a Hash with a symbol as its only key and a Proc as its only value' do
216
+ env = {}
217
+ router = Class.new MessageRouter::Router do
218
+ match :true_method => (Proc.new { env['did_it_run'] = true })
219
+ def true_method; true; end
220
+ end.new
221
+ router.call env
222
+ env['did_it_run'].should be_true
223
+ end
68
224
 
69
- match /hi halt (\w+)/ do |word|
70
- halt word
225
+ it 'raises an execption when no arguments and no block is given' do
226
+ lambda {
227
+ router = Class.new MessageRouter::Router do
228
+ match
229
+ end.new
230
+ }.should raise_error(ArgumentError)
231
+ end
71
232
  end
233
+ end
72
234
 
73
- match /hi halt/ do
74
- halt
235
+
236
+ describe "#call" do
237
+ it "returns nil with no rules" do
238
+ r = MessageRouter::Router.new
239
+ r.call({}).should be_nil
75
240
  end
76
-
77
- match /hi (\w+)/ do |name|
78
- halt "how do you do #{name}"
241
+
242
+ context 'a rule matches' do
243
+ subject do
244
+ Class.new MessageRouter::Router do
245
+ match(true) { env[:did_it_run] = true }
246
+ end.new
247
+ end
248
+
249
+ it "returns true" do
250
+ subject.call({}).should be_true
251
+ end
252
+
253
+ it "calls the matcher's code" do
254
+ subject.call(env = {})
255
+ env[:did_it_run].should be_true
256
+ end
79
257
  end
80
-
81
- match /hola (\w+) (\w+)/, :from => 'bradgessler' do |first_name, last_name|
82
- halt "hello #{first_name} #{last_name} in spanish"
258
+
259
+ context 'there is a prerequisite which is true' do
260
+ subject do
261
+ Class.new MessageRouter::Router do
262
+ prerequisite :true_method
263
+ match(true) { env[:did_it_run] = true }
264
+ def true_method; true; end
265
+ end.new
266
+ end
267
+
268
+ it "returns true" do
269
+ subject.call({}).should be_true
270
+ end
271
+
272
+ it "calls the matcher's code" do
273
+ subject.call(env = {})
274
+ env[:did_it_run].should be_true
275
+ end
83
276
  end
84
-
85
- private
86
- def all_caps_with_numbers
87
- if message[:body] =~ /^[A-Z0-9\s]+$/
88
- ["Zeldzamar", 1080]
277
+
278
+ context 'there is a prerequisite which is false' do
279
+ subject do
280
+ Class.new MessageRouter::Router do
281
+ prerequisite :false_method
282
+ match(true) { env[:did_it_run] = true }
283
+ def false_method; false; end
284
+ end.new
285
+ end
286
+
287
+ it "returns false" do
288
+ subject.call({}).should be_false
289
+ end
290
+
291
+ it "doesn't calls the matcher's code" do
292
+ subject.call(env = {})
293
+ env[:did_it_run].should_not be_true
89
294
  end
90
295
  end
91
- end
92
296
 
93
- def dispatch(*args)
94
- reply = TwitterRouter.dispatch(*args)
95
- reply ? reply[:body] : nil
96
- end
297
+ describe 'nested routers' do
298
+ def main_router
299
+ Class.new(MessageRouter::Router) do
300
+ sub_router = Class.new(MessageRouter::Router) do
301
+ match($inner_matcher) { $did_inner_run = true }
302
+ end.new
97
303
 
98
- it "should return nil if there are no matches" do
99
- dispatch("bums").should be_nil
100
- end
304
+ match $outer_matcher do
305
+ $did_outer_run = true
306
+ sub_router.call(env)
307
+ end
308
+ end.new
309
+ end
101
310
 
102
- it "should have default key" do
103
- TwitterRouter.new.default_key.should eql(:body)
104
- end
311
+ before do
312
+ $outer_matcher = $inner_matcher = $did_outer_run = $did_inner_run = nil
313
+ end
105
314
 
106
- context "mounted router" do
107
- it "should process message" do
108
- dispatch("crazy").should eql("factory blow out sales are awesome")
109
- end
110
- end
315
+ it 'runs both when both match' do
316
+ $outer_matcher = $inner_matcher = true
111
317
 
112
- context "should halt" do
113
- it "without value" do
114
- dispatch("hi halt").should be_nil
115
- end
318
+ main_router.call({}).should be_true
319
+ $did_outer_run.should be_true
320
+ $did_inner_run.should be_true
321
+ end
116
322
 
117
- it "with value" do
118
- dispatch("hi halt narf").should eql("narf")
119
- end
120
- end
121
-
122
- context "default matcher" do
123
- it "should capture regexps" do
124
- dispatch('hi dude').should eql('pleased to meet you')
125
- end
126
-
127
- it "should pass regexp captures through blocks" do
128
- dispatch('hi brad').should eql("how do you do brad")
129
- end
130
- end
131
-
132
- context "hash matcher" do
133
- it "should capture with default matcher" do
134
- dispatch('hola jeannette gessler', :from => 'bradgessler').should eql("hello jeannette gessler in spanish")
135
- end
323
+ it "runs outer only when outer matches and inner doesn't" do
324
+ $outer_matcher = true
325
+ $inner_matcher = false
136
326
 
137
- it "should capture with an explicit hash" do
138
- dispatch(:body => 'hola jeannette gessler', :from => 'bradgessler').should eql("hello jeannette gessler in spanish")
139
- end
140
- end
141
-
142
- context "context" do
143
- it "should handle contexts and non-proc conditions" do
144
- dispatch('HI BRAD 90').should eql("STOP SHOUTING WITH NUMBERS!")
145
- end
146
-
147
- it "should handle nested contexts and proc conditions" do
148
- dispatch('HI BRAD').should eql("STOP SHOUTING WITHOUT NUMBERS!")
327
+ main_router.call({}).should be_nil
328
+ $did_outer_run.should be_true
329
+ $did_inner_run.should be_nil
330
+ end
331
+
332
+ it "runs neither when inner matches and outer doesn't" do
333
+ $outer_matcher = false
334
+ $inner_matcher = true
335
+
336
+ main_router.call({}).should be_nil
337
+ $did_outer_run.should be_nil
338
+ $did_inner_run.should be_nil
339
+ end
340
+
341
+ context 'multiple inner matchers' do
342
+ before do
343
+ $outer_matcher_1 = $outer_matcher_2 = $inner_matcher_1 = $inner_matcher_2 = $did_outer_run_1 = $did_outer_run_2 = $did_inner_run_1 = $did_inner_run_2 = nil
344
+ end
345
+
346
+ def main_router
347
+ Class.new MessageRouter::Router do
348
+ # Define them
349
+ sub_router_1 = Class.new MessageRouter::Router do
350
+ match($inner_matcher_1) { $did_inner_run_1 = true }
351
+ end.new
352
+ sub_router_2 = Class.new MessageRouter::Router do
353
+ match($inner_matcher_2) { $did_inner_run_2 = true }
354
+ end.new
355
+
356
+ # 'mount' them
357
+ match $outer_matcher_1 do
358
+ $did_outer_run_1 = true
359
+ sub_router_1.call(env)
360
+ end
361
+
362
+ match $outer_matcher_2 do
363
+ $did_outer_run_2 = true
364
+ sub_router_2.call(env)
365
+ end
366
+ end.new
367
+ end
368
+
369
+ it "runs only 1st outer and 1st inner when all match" do
370
+ $outer_matcher_1 = $outer_matcher_2 = $inner_matcher_1 = $inner_matcher_2 = true
371
+
372
+ main_router.call({}).should be_true
373
+ $did_outer_run_1.should be_true
374
+ $did_outer_run_2.should be_nil
375
+ $did_inner_run_1.should be_true
376
+ $did_inner_run_2.should be_nil
377
+ end
378
+
379
+ it "runs both outers, and 2nd inner when all but 1st inner match" do
380
+ $outer_matcher_1 = $outer_matcher_2 = $inner_matcher_2 = true
381
+ $inner_matcher_1 = false
382
+
383
+ main_router.call({}).should be_true
384
+ $did_outer_run_1.should be_true
385
+ $did_outer_run_2.should be_true
386
+ $did_inner_run_1.should be_nil
387
+ $did_inner_run_2.should be_true
388
+ end
389
+
390
+ end
149
391
  end
150
-
151
- it "should pass arguments into contexts" do
152
- dispatch('FUNNYWORD').should eql("Zeldzamar-1080")
392
+
393
+
394
+ describe 'helper methods' do
395
+ module MyTestHelper
396
+ LOOKUP = {
397
+ 1 => 'John',
398
+ 2 => 'Jim',
399
+ 3 => 'Jules'
400
+ }
401
+ def lookup_human_name
402
+ env['human_name'] = LOOKUP[env['id']]
403
+ end
404
+ end
405
+
406
+ let :router do
407
+ Class.new MessageRouter::Router do
408
+ include MyTestHelper
409
+ match :lookup_human_name do
410
+ $is_john = env['human_name'] == 'John'
411
+ end
412
+
413
+ match 'run_a' => 'block' do
414
+ env['id'] = 2
415
+ env['the_name'] = lookup_human_name
416
+ end
417
+ match({'run_a' => 'proc'}, Proc.new do
418
+ env['id'] = 2
419
+ env['the_name'] = lookup_human_name
420
+ end)
421
+ match({'run_a' => 'lambda'}, lambda do
422
+ env['id'] = 2
423
+ env['the_name'] = lookup_human_name
424
+ end)
425
+
426
+ match(
427
+ Proc.new do
428
+ env['id'] = 3 if %w(proc lambda).include?(env['match_with'])
429
+ lookup_human_name
430
+ end
431
+ ) { true }
432
+ end.new
433
+ end
434
+
435
+ it 'can access/modify the env via #env' do
436
+ env = {'id' => 1}
437
+ router.call(env).should be_true
438
+ $is_john.should be_true # Prove the inner matcher can see the new value
439
+ env['human_name'].should == 'John' # Prove we can get at the value after the router has finished.
440
+ end
441
+
442
+ it '#env is reset after #call has finished' do
443
+ router.call({'id' => 1}).should be_true
444
+ router.send(:env).should be_nil
445
+ end
446
+
447
+ %w(block proc lambda).each do |type|
448
+ it "can be accessed from a #{type} that is the 2nd argument" do
449
+ env = {'run_a' => type}
450
+ router.call(env).should be_true
451
+ env['the_name'].should == 'Jim'
452
+ end
453
+ end
454
+
455
+ %w(proc lambda).each do |type|
456
+ it "can be accessed from a #{type} that is the 1st argument" do
457
+ env = {'match_with' => type}
458
+ router.call(env).should be_true
459
+ env['human_name'].should == 'Jules'
460
+ end
461
+ end
153
462
  end
154
463
  end
155
- end
464
+ end