runt 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -1,122 +1,122 @@
1
- # Rakefile for runt -*- ruby -*-
2
-
3
- begin
4
- require 'rubygems'
5
- require 'rake/gempackagetask'
6
- rescue Exception
7
- nil
8
- end
9
- require 'rake'
10
- require 'rake/clean'
11
- require 'rake/testtask'
12
- require 'rake/rdoctask'
13
- require 'rake/contrib/sshpublisher'
14
- require 'rake/contrib/rubyforgepublisher'
15
- require 'fileutils'
16
-
17
- #####################################################################
18
- # Constants
19
- #####################################################################
20
-
21
- # Build Settings
22
- PKG_VERSION = "0.6.0"
23
-
24
- # Files to be included in Runt distribution
25
- PKG_FILES = FileList[
26
- 'setup.rb',
27
- '[A-Z]*',
28
- 'lib/**/*.rb',
29
- 'test/**/*.rb',
30
- 'examples/**/*.rb',
31
- 'doc/**/*',
32
- 'site/**/*'
33
- ].exclude("*.ses")
34
-
35
- if(RUBY_PLATFORM =~ /win32/i)
36
- PKG_EXEC_TAR = false
37
- else
38
- PKG_EXEC_TAR = true
39
- end
40
-
41
- # build directory
42
- TARGET_DIR = "target"
43
-
44
- #####################################################################
45
- # Targets
46
- #####################################################################
47
-
48
- task :default => [:test]
49
- task :clobber => [:clobber_build_dir]
50
-
51
- # Make the build directory
52
- directory TARGET_DIR
53
-
54
- # Calling this task directly doesn't work?
55
- desc "Clobber the entire build directory."
56
- task :clobber_build_dir do |t|
57
- CLOBBER.include(TARGET_DIR)
58
- end
59
-
60
- Rake::RDocTask.new do |rd|
61
- rd.rdoc_dir="#{TARGET_DIR}/doc"
62
- rd.options << "-S"
63
- rd.rdoc_files.include('lib/*','doc/*.rdoc','README','CHANGES','TODO','LICENSE.txt')
64
- end
65
-
66
- Rake::TestTask.new do |t|
67
- t.libs << "test" << "examples"
68
- t.pattern = '**/*test.rb'
69
- t.verbose = false
70
- end
71
-
72
- desc "Copy html files for the Runt website to the build directory."
73
- file "copy_site" => TARGET_DIR
74
- file "copy_site" do
75
- cp_r Dir["site/*.{html,gif,png,css}"], TARGET_DIR
76
- end
77
-
78
- desc "Publish the Documentation to RubyForge."
79
- task :publish => [:rdoc,:copy_site,:clobber_package] do |t|
80
- publisher = Rake::CompositePublisher.new
81
- publisher.add Rake::SshDirPublisher.new("mlipper@rubyforge.org", "/var/www/gforge-projects/runt",TARGET_DIR)
82
- publisher.upload
83
- end
84
-
85
- desc "Publish the Documentation to the build dir."
86
- task :test_publish => [:rdoc,:copy_site,:clobber_package] do |t|
87
- puts "YAY! We've tested publish! YAY!"
88
- end
89
-
90
-
91
- if ! defined?(Gem)
92
- puts "Package Target requires RubyGEMs"
93
- else
94
- spec = Gem::Specification.new do |s|
95
- s.platform = Gem::Platform::RUBY
96
- s.summary = "Ruby Temporal Expressions."
97
- s.name = 'runt'
98
- s.version = PKG_VERSION
99
- s.requirements << 'none'
100
- s.require_path = 'lib'
101
- s.autorequire = 'runt'
102
- s.files = PKG_FILES.to_a
103
- s.author = 'Matthew Lipper'
104
- s.email = 'mlipper@gmail.com'
105
- s.homepage = 'http://runt.rubyforge.org'
106
- s.has_rdoc = true
107
- s.rdoc_options += %w{--main README --title Runt}
108
- s.extra_rdoc_files = FileList["README","CHANGES","TODO","LICENSE.txt","doc/*.rdoc"]
109
- s.test_files = Dir['**/*test.rb']
110
- s.rubyforge_project = 'runt'
111
- s.description = <<EOF
112
- Runt is a Ruby version of temporal patterns by
113
- Martin Fowler. Runt provides an API for scheduling
114
- recurring events using set-like semantics.
115
- EOF
116
- end
117
-
118
- Rake::GemPackageTask.new(spec) do |pkg|
119
- pkg.need_zip = true
120
- pkg.need_tar = PKG_EXEC_TAR
121
- end
122
- end
1
+ # Rakefile for runt -*- ruby -*-
2
+
3
+ begin
4
+ require 'rubygems'
5
+ require 'rake/gempackagetask'
6
+ rescue Exception
7
+ nil
8
+ end
9
+ require 'rake'
10
+ require 'rake/clean'
11
+ require 'rake/testtask'
12
+ require 'rake/rdoctask'
13
+ require 'rake/contrib/sshpublisher'
14
+ require 'rake/contrib/rubyforgepublisher'
15
+ require 'fileutils'
16
+
17
+ #####################################################################
18
+ # Constants
19
+ #####################################################################
20
+
21
+ # Build Settings
22
+ PKG_VERSION = "0.7.0"
23
+
24
+ # Files to be included in Runt distribution
25
+ PKG_FILES = FileList[
26
+ 'setup.rb',
27
+ '[A-Z]*',
28
+ 'lib/**/*.rb',
29
+ 'test/**/*.rb',
30
+ 'examples/**/*.rb',
31
+ 'doc/**/*',
32
+ 'site/**/*'
33
+ ].exclude("*.ses")
34
+
35
+ if(RUBY_PLATFORM =~ /win32/i)
36
+ PKG_EXEC_TAR = false
37
+ else
38
+ PKG_EXEC_TAR = true
39
+ end
40
+
41
+ # build directory
42
+ TARGET_DIR = "target"
43
+
44
+ #####################################################################
45
+ # Targets
46
+ #####################################################################
47
+
48
+ task :default => [:test]
49
+ task :clobber => [:clobber_build_dir]
50
+
51
+ # Make the build directory
52
+ directory TARGET_DIR
53
+
54
+ # Calling this task directly doesn't work?
55
+ desc "Clobber the entire build directory."
56
+ task :clobber_build_dir do |t|
57
+ CLOBBER.include(TARGET_DIR)
58
+ end
59
+
60
+ Rake::RDocTask.new do |rd|
61
+ rd.rdoc_dir="#{TARGET_DIR}/doc"
62
+ rd.options << "-S"
63
+ rd.rdoc_files.include('lib/*','doc/*.rdoc','README','CHANGES','TODO','LICENSE.txt')
64
+ end
65
+
66
+ Rake::TestTask.new do |t|
67
+ t.libs << "test" << "examples"
68
+ t.pattern = '**/*test.rb'
69
+ t.verbose = false
70
+ t.warning = false
71
+ end
72
+
73
+ desc "Copy html files for the Runt website to the build directory."
74
+ file "copy_site" => TARGET_DIR
75
+ file "copy_site" do
76
+ cp_r Dir["site/*.{html,gif,png,css}"], TARGET_DIR
77
+ end
78
+
79
+ desc "Publish the Documentation to RubyForge."
80
+ task :publish => [:rdoc,:copy_site,:clobber_package] do |t|
81
+ publisher = Rake::CompositePublisher.new
82
+ publisher.add Rake::SshDirPublisher.new("mlipper@rubyforge.org", "/var/www/gforge-projects/runt",TARGET_DIR)
83
+ publisher.upload
84
+ end
85
+
86
+ desc "Publish the Documentation to the build dir."
87
+ task :test_publish => [:rdoc,:copy_site,:clobber_package] do |t|
88
+ puts "YAY! We've tested publish! YAY!"
89
+ end
90
+
91
+
92
+ if ! defined?(Gem)
93
+ puts "Package Target requires RubyGEMs"
94
+ else
95
+ spec = Gem::Specification.new do |s|
96
+ s.platform = Gem::Platform::RUBY
97
+ s.summary = "Ruby Temporal Expressions."
98
+ s.name = 'runt'
99
+ s.version = PKG_VERSION
100
+ s.requirements << 'none'
101
+ s.require_path = 'lib'
102
+ s.files = PKG_FILES.to_a
103
+ s.author = 'Matthew Lipper'
104
+ s.email = 'mlipper@gmail.com'
105
+ s.homepage = 'http://runt.rubyforge.org'
106
+ s.has_rdoc = true
107
+ s.rdoc_options += %w{--main README --title Runt}
108
+ s.extra_rdoc_files = FileList["README","CHANGES","TODO","LICENSE.txt","doc/*.rdoc"]
109
+ s.test_files = Dir['**/*test.rb']
110
+ s.rubyforge_project = 'runt'
111
+ s.description = <<EOF
112
+ Runt is a Ruby version of temporal patterns by
113
+ Martin Fowler. Runt provides an API for scheduling
114
+ recurring events using set-like semantics.
115
+ EOF
116
+ end
117
+
118
+ Rake::GemPackageTask.new(spec) do |pkg|
119
+ pkg.need_zip = true
120
+ pkg.need_tar = PKG_EXEC_TAR
121
+ end
122
+ end
data/TODO CHANGED
@@ -1,13 +1,13 @@
1
- = Runt - Ruby Temporal Expressions -- To Do List
2
-
3
- Send suggestions, questions, threats, etc. for this list to Matt[mailto:mlipper@gmail.com]
4
-
5
- === To Do
6
-
7
- * WIMonth#dates behaves unintuitively (see dates mixin tests)
8
-
9
- * DayIntervalTE matches date multiples prior to start date (see tests)
10
-
11
- * Better docs, examples, tutorials
12
-
13
- * Laundry
1
+ = Runt - Ruby Temporal Expressions -- To Do List
2
+
3
+ Send suggestions, questions, threats, etc. for this list to Matt[mailto:mlipper@gmail.com]
4
+
5
+ === To Do
6
+
7
+ * WIMonth#dates behaves unintuitively (see dates mixin tests)
8
+
9
+ * DayIntervalTE matches date multiples prior to start date (see tests)
10
+
11
+ * Better docs, examples, tutorials
12
+
13
+ * Laundry
@@ -1,393 +1,393 @@
1
- = Schedule Tutorial
2
-
3
- <em> This tutorial assumes you are familiar with use of the Runt API to
4
- create temporal expressions. If you're unfamiliar with how and why to write
5
- temporal expressions, take a look at the temporal expression
6
- tutorial[http://runt.rubyforge.org/doc/files/doc/tutorial_te_rdoc.html].</em>
7
-
8
- In his paper[http://martinfowler.com/apsupp/recurring.pdf] about recurring
9
- events, Martin Fowler also discusses a simple schedule API which is used,
10
- surprisingly enough, to build a schedule. We're not going to cover the pattern
11
- itself in this tutorial as Fowler already does a nice job. Because it is such
12
- a simple pattern (once you invent it!), you'll be able understand it even if
13
- you decide not to read his paper[http://martinfowler.com/apsupp/recurring.pdf].
14
-
15
- So, let's pretend that I own a car. Since I don't want to get a ticket, I
16
- decide to create an application which will tell me where and when I can park
17
- it on my street. (Since this is all make believe anyway, my car is a late 60's
18
- model black Ford Mustang with flame detailing (and on the back seat is one
19
- million dollars)).
20
-
21
- We'll build a Runt Schedule that models the parking regulations. Our app
22
- will check this Schedule at regular intervals and send us reminders to
23
- move our car so we don't get a ticket. YAY!
24
-
25
- First, let's visit the exciting world of NYC street cleaning regulations.
26
- Let's pretend the following rules are in place for our block:
27
-
28
- * For the north side of the street, there is no parking Monday, Wednesday, or Friday, from 8am thru 11am
29
-
30
- * For the south side of the street, there is no parking Tuesday or Thursday between 11:30am and 2pm
31
-
32
- Thus...
33
-
34
- ############################# #############################
35
- # # # #
36
- # NO PARKING # # NO PARKING #
37
- # # # #
38
- # Mon, Wed, Fri 8am-11am # # Tu, Th 11:30am-2:00pm #
39
- # # # #
40
- # # # #
41
- # Violators will be towed! # # Violaters will be towed! #
42
- # # # #
43
- ############################# #############################
44
- # # # #
45
- # # # #
46
- # # # #
47
-
48
- North side of the street South side of the street
49
-
50
-
51
- We'll start by creating temporal expressions which describe the verboten
52
- parking times:
53
-
54
-
55
- north_expr = (DIWeek.new(Mon) | DIWeek.new(Wed) | DIWeek.new(Fri)) & REDay.new(8,00,11,00)
56
-
57
- south_expr = (DIWeek.new(Tue) | DIWeek.new(Thu)) & REDay.new(11,30,14,00)
58
-
59
-
60
- What we need at this point is a way to write queries against these expressions
61
- to determine whether we need to send a reminder. For this purpose, we can use
62
- a Schedule and an associated Event, both of which are supplied by Runt.
63
-
64
- schedule = Schedule.new
65
-
66
- A Schedule holds zero or more Event/TemporalExpression pairs, allowing clients
67
- to easily query and update TemporalExpressions as well perform certain range
68
- operations as we will see in a moment. We'll create two events, one for each
69
- side of the street:
70
-
71
- north_event = Event.new("north side")
72
-
73
- south_event = Event.new("south side")
74
-
75
- Now we add each event and its associated occurrence to our Schedule:
76
-
77
- schedule.add(north_event, north_expr)
78
-
79
- schedule.add(south_event, south_expr)
80
-
81
- An Event is simply a container for domain data. Although Runt uses Events
82
- by default, Schedules will happily house any kind of Object. Internally, a
83
- Schedule is really just a Hash where the keys are whatever it is you are
84
- scheduling and the values are the TemporalExpressions you create.
85
-
86
- class Schedule
87
- ...
88
-
89
- def add(obj, expression)
90
- @elems[obj]=expression
91
- end
92
- ...
93
-
94
- Now that we have a Schedule configured, we need something to check it and
95
- then let us know if we need to move the car. For this, we'll create a simple
96
- class called Reminder which will function as the "main-able" part of
97
- our app.
98
-
99
- We'll start by creating an easily testable constructor which will be passed
100
- a Schedule instance (like the one we just created) and an SMTP server.
101
-
102
-
103
- class Reminder
104
-
105
- attr_reader :schedule, :mail_server
106
-
107
- def initialize(schedule,mail_server)
108
- @schedule = schedule
109
- @mail_server = mail_server
110
- end
111
- ...
112
-
113
- Being devoted Agilists, we'll of course also create a unit test to
114
- help flesh out the specifics of our new Reminder class. We'll create
115
- test fixtures using the Runt Objects described above.
116
-
117
-
118
- class ReminderTest < Test::Unit::TestCase
119
-
120
- include Runt
121
-
122
- def setup
123
- @schedule = Schedule.new
124
- @north_event = Event.new("north side of the street will be ticketed")
125
- north_expr = (DIWeek.new(Mon) | DIWeek.new(Wed) | DIWeek.new(Fri)) & REDay.new(8,00,11,00)
126
- @schedule.add(@north_event, north_expr)
127
- @south_event = Event.new("south side of the street will be ticketed")
128
- south_expr = (DIWeek.new(Tue) | DIWeek.new(Thu)) & REDay.new(11,30,14,00)
129
- @schedule.add(@south_event, south_expr)
130
- @mail_server = MailServer.new
131
- @reminder = Reminder.new(@schedule, @mail_server)
132
- @saturday_at_10 = PDate.min(2007,11,24,10,0,0)
133
- @monday_at_10 = PDate.min(2007,11,26,10,0,0)
134
- @tuesday_at_noon = PDate.min(2007,11,27,12,0,0)
135
- end
136
-
137
- def test_initalize
138
- assert_same @schedule, @reminder.schedule, "Expected #{@schedule} instead was #{@reminder.schedule}"
139
- assert_same @mail_server, @reminder.mail_server, "Expected #{@mail_server} instead was #{@reminder.mail_server}"
140
- end
141
- ...
142
-
143
- For the purposes of this tutorial, the mail server will simply be a stub to
144
- illustrate how a real one might be used.
145
-
146
- class MailServer
147
-
148
- Struct.new("Email",:to,:from,:subject,:text)
149
-
150
- def send(to, from, subject, text)
151
- Struct::Email.new(to, from, subject, text)
152
- # etc...
153
- end
154
-
155
- end
156
-
157
- Next, let's add a method to our Reminder class which actually checks our
158
- schedule using a date which is passed in as a parameter.
159
-
160
- class Reminder
161
- ...
162
- def check(date)
163
- return @schedule.events(date)
164
- end
165
- ...
166
-
167
- The Schedule#events method will return an Array of Event Objects for any
168
- events which occur at the date and time given by the method's argument. Usage
169
- is easily demonstrated by a test case which makes use of the fixtures created
170
- by the TestCase#setup method defined above.
171
-
172
- class ReminderTest < Test::Unit::TestCase
173
- ...
174
- def test_check
175
- assert_equal 1, @reminder.check(@monday_at_10).size, "Unexpected size #{@reminder.check(@monday_at_10).size} returned"
176
- assert_same @north_event, @reminder.check(@monday_at_10)[0], "Expected Event #{@north_event}. Got #{@reminder.check(@monday_at_10)[0]}."
177
- assert_equal 1, @reminder.check(@tuesday_at_noon).size, "Unexpected size #{@reminder.check(@tuesday_at_noon).size} returned"
178
- assert_same @south_event, @reminder.check(@tuesday_at_noon)[0], "Expected Event #{@south_event}. Got #{@reminder.check(@tuesday_at_noon)[0]}."
179
- assert @reminder.check(@saturday_at_10).empty?, "Expected empty Array. Got #{@reminder.check(@saturday_at_10)}"
180
- end
181
- ...
182
-
183
-
184
- There are other methods in the Schedule API which allow a client to query for
185
- information. Although we don't need them for this tutorial, I'll mention two
186
- briefly because they are generally useful. The first is Schedule#dates
187
- which will return an Array of PDate Objects which occur during the DateRange
188
- supplied as a parameter. The second is Schedule#include? which returns a
189
- boolean value indicating whether the Event occurs on the date which are both
190
- supplied as arguments.
191
-
192
- Next, let's make use of the mail server argument given to the Reminder class
193
- in it's constructor. This is the method that will be called when a call to the
194
- Reminder#check method produces results.
195
-
196
- class Reminder
197
- ...
198
- def send(date)
199
- text = "Warning: " + events.join(', ')
200
- return @mail_server.send(TO, FROM, SUBJECT, text)
201
- end
202
- ...
203
-
204
-
205
- Testing this is simple thanks to our MailServer stub which simply regurgitates
206
- the text argument it's passed as a result.
207
-
208
- class ReminderTest < Test::Unit::TestCase
209
- ...
210
- def test_send
211
- params = [@north_event, @south_event]
212
- result = @reminder.send(params)
213
- assert_email result, Reminder::TEXT + params.join(', ')
214
- end
215
-
216
- def assert_email(result, text)
217
- assert_equal Reminder::TO, result.to, "Unexpected value for 'to' field of Email Struct: #{result.to}"
218
- assert_equal Reminder::FROM, result.from, "Unexpected value for 'from' field of Email Struct: #{result.from}"
219
- assert_equal Reminder::SUBJECT, result.subject, "Unexpected value for 'subject' field of Email Struct: #{result.subject}"
220
- assert_equal text, result.text, "Unexpected value for 'text' field of Email Struct: #{result.text}"
221
- end
222
- ...
223
-
224
- Note the ReminderTest#assert_email method we've added to make assertions
225
- common to multiple test cases.
226
-
227
-
228
- Now, let's tie the whole thing together with a method which which checks for
229
- occuring Events and (upon finding some) sends a reminder. This method is
230
- really the only one in the Reminder class that needs to be public.
231
-
232
- class Reminder
233
- ...
234
- def run(date)
235
- result = self.check(date)
236
- self.send(result) if !result.empty?
237
- end
238
- ...
239
-
240
- class ReminderTest < Test::Unit::TestCase
241
- ...
242
- def test_send
243
- params = [@north_event, @south_event]
244
- result = @reminder.send(params)
245
- assert_email result, Reminder::TEXT + params.join(', ')
246
- end
247
- ...
248
-
249
- Finally, we'll cheat a bit and stitch every thing together so it can be run
250
- from a command line.
251
-
252
-
253
- include Runt
254
-
255
- schedule = Schedule.new
256
- north_event = Event.new("north side")
257
- north_expr = (DIWeek.new(Mon) | DIWeek.new(Wed) | DIWeek.new(Fri)) & REDay.new(8,00,11,00)
258
- schedule.add(north_event, north_expr)
259
- south_event = Event.new("south side")
260
- south_expr = (DIWeek.new(Tue) | DIWeek.new(Thu)) & REDay.new(11,30,14,00)
261
- schedule.add(south_event, south_expr)
262
- reminder = Reminder.new(schedule, MailServer.new)
263
- while true
264
- sleep 15.minutes
265
- reminder.run Time.now
266
- end
267
-
268
- So, here's all the code for this tutorial (it's in the Runt distribution under
269
- the examples folder):
270
-
271
- ### schedule_tutorial.rb ###
272
-
273
- #!/usr/bin/ruby
274
-
275
- require 'runt'
276
-
277
- class Reminder
278
-
279
- TO = "me@myselfandi.com"
280
- FROM = "reminder@daemon.net"
281
- SUBJECT = "Move your car!"
282
- TEXT = "Warning: "
283
-
284
- attr_reader :schedule, :mail_server
285
-
286
- def initialize(schedule,mail_server)
287
- @schedule = schedule
288
- @mail_server = mail_server
289
- end
290
- def run(date)
291
- result = self.check(date)
292
- self.send(result) if !result.empty?
293
- end
294
- def check(date)
295
- puts "Checking the schedule..." if $DEBUG
296
- return @schedule.events(date)
297
- end
298
- def send(events)
299
- text = TEXT + events.join(', ')
300
- return @mail_server.send(TO, FROM, SUBJECT, text)
301
- end
302
- end
303
-
304
- class MailServer
305
- Struct.new("Email",:to,:from,:subject,:text)
306
- def send(to, from, subject, text)
307
- puts "Sending message TO: #{to} FROM: #{from} RE: #{subject}..." if $DEBUG
308
- Struct::Email.new(to, from, subject, text)
309
- # etc...
310
- end
311
- end
312
-
313
-
314
- if __FILE__ == $0
315
-
316
- include Runt
317
-
318
- schedule = Schedule.new
319
- north_event = Event.new("north side")
320
- north_expr = (DIWeek.new(Mon) | DIWeek.new(Wed) | DIWeek.new(Fri)) & REDay.new(8,00,11,00)
321
- schedule.add(north_event, north_expr)
322
- south_event = Event.new("south side")
323
- south_expr = (DIWeek.new(Tue) | DIWeek.new(Thu)) & REDay.new(11,30,14,00)
324
- schedule.add(south_event, south_expr)
325
- reminder = Reminder.new(schedule, MailServer.new)
326
- while true
327
- sleep 15.minutes
328
- reminder.run Time.now
329
- end
330
-
331
- end
332
-
333
-
334
- ### schedule_tutorialtest.rb ###
335
-
336
- #!/usr/bin/ruby
337
-
338
- require 'test/unit'
339
- require 'runt'
340
- require 'schedule_tutorial'
341
-
342
- class ReminderTest < Test::Unit::TestCase
343
-
344
- include Runt
345
-
346
- def setup
347
- @schedule = Schedule.new
348
- @north_event = Event.new("north side of the street will be ticketed")
349
- north_expr = (DIWeek.new(Mon) | DIWeek.new(Wed) | DIWeek.new(Fri)) & REDay.new(8,00,11,00)
350
- @schedule.add(@north_event, north_expr)
351
- @south_event = Event.new("south side of the street will be ticketed")
352
- south_expr = (DIWeek.new(Tue) | DIWeek.new(Thu)) & REDay.new(11,30,14,00)
353
- @schedule.add(@south_event, south_expr)
354
- @mail_server = MailServer.new
355
- @reminder = Reminder.new(@schedule, @mail_server)
356
- @saturday_at_10 = PDate.min(2007,11,24,10,0,0)
357
- @monday_at_10 = PDate.min(2007,11,26,10,0,0)
358
- @tuesday_at_noon = PDate.min(2007,11,27,12,0,0)
359
- end
360
- def test_initalize
361
- assert_same @schedule, @reminder.schedule, "Expected #{@schedule} instead was #{@reminder.schedule}"
362
- assert_same @mail_server, @reminder.mail_server, "Expected #{@mail_server} instead was #{@reminder.mail_server}"
363
- end
364
- def test_send
365
- params = [@north_event, @south_event]
366
- result = @reminder.send(params)
367
- assert_email result, Reminder::TEXT + params.join(', ')
368
- end
369
- def test_check
370
- assert_equal 1, @reminder.check(@monday_at_10).size, "Unexpected size #{@reminder.check(@monday_at_10).size} returned"
371
- assert_same @north_event, @reminder.check(@monday_at_10)[0], "Expected Event #{@north_event}. Got #{@reminder.check(@monday_at_10)[0]}."
372
- assert_equal 1, @reminder.check(@tuesday_at_noon).size, "Unexpected size #{@reminder.check(@tuesday_at_noon).size} returned"
373
- assert_same @south_event, @reminder.check(@tuesday_at_noon)[0], "Expected Event #{@south_event}. Got #{@reminder.check(@tuesday_at_noon)[0]}."
374
- assert @reminder.check(@saturday_at_10).empty?, "Expected empty Array. Got #{@reminder.check(@saturday_at_10)}"
375
- end
376
- def test_run
377
- result = @reminder.run(@monday_at_10)
378
- assert_email result, Reminder::TEXT + @north_event.to_s
379
- end
380
- def assert_email(result, text)
381
- assert_equal Reminder::TO, result.to, "Unexpected value for 'to' field of Email Struct: #{result.to}"
382
- assert_equal Reminder::FROM, result.from, "Unexpected value for 'from' field of Email Struct: #{result.from}"
383
- assert_equal Reminder::SUBJECT, result.subject, "Unexpected value for 'subject' field of Email Struct: #{result.subject}"
384
- assert_equal text, result.text, "Unexpected value for 'text' field of Email Struct: #{result.text}"
385
- end
386
- end
387
-
388
-
389
- <em>See Also:</em>
390
-
391
- * Fowler's recurring event pattern[http://martinfowler.com/apsupp/recurring.pdf]
392
-
393
- * Other temporal patterns[http://martinfowler.com/ap2/timeNarrative.html]
1
+ = Schedule Tutorial
2
+
3
+ <em> This tutorial assumes you are familiar with use of the Runt API to
4
+ create temporal expressions. If you're unfamiliar with how and why to write
5
+ temporal expressions, take a look at the temporal expression
6
+ tutorial[http://runt.rubyforge.org/doc/files/doc/tutorial_te_rdoc.html].</em>
7
+
8
+ In his paper[http://martinfowler.com/apsupp/recurring.pdf] about recurring
9
+ events, Martin Fowler also discusses a simple schedule API which is used,
10
+ surprisingly enough, to build a schedule. We're not going to cover the pattern
11
+ itself in this tutorial as Fowler already does a nice job. Because it is such
12
+ a simple pattern (once you invent it!), you'll be able understand it even if
13
+ you decide not to read his paper[http://martinfowler.com/apsupp/recurring.pdf].
14
+
15
+ So, let's pretend that I own a car. Since I don't want to get a ticket, I
16
+ decide to create an application which will tell me where and when I can park
17
+ it on my street. (Since this is all make believe anyway, my car is a late 60's
18
+ model black Ford Mustang with flame detailing (and on the back seat is one
19
+ million dollars)).
20
+
21
+ We'll build a Runt Schedule that models the parking regulations. Our app
22
+ will check this Schedule at regular intervals and send us reminders to
23
+ move our car so we don't get a ticket. YAY!
24
+
25
+ First, let's visit the exciting world of NYC street cleaning regulations.
26
+ Let's pretend the following rules are in place for our block:
27
+
28
+ * For the north side of the street, there is no parking Monday, Wednesday, or Friday, from 8am thru 11am
29
+
30
+ * For the south side of the street, there is no parking Tuesday or Thursday between 11:30am and 2pm
31
+
32
+ Thus...
33
+
34
+ ############################# #############################
35
+ # # # #
36
+ # NO PARKING # # NO PARKING #
37
+ # # # #
38
+ # Mon, Wed, Fri 8am-11am # # Tu, Th 11:30am-2:00pm #
39
+ # # # #
40
+ # # # #
41
+ # Violators will be towed! # # Violaters will be towed! #
42
+ # # # #
43
+ ############################# #############################
44
+ # # # #
45
+ # # # #
46
+ # # # #
47
+
48
+ North side of the street South side of the street
49
+
50
+
51
+ We'll start by creating temporal expressions which describe the verboten
52
+ parking times:
53
+
54
+
55
+ north_expr = (DIWeek.new(Mon) | DIWeek.new(Wed) | DIWeek.new(Fri)) & REDay.new(8,00,11,00)
56
+
57
+ south_expr = (DIWeek.new(Tue) | DIWeek.new(Thu)) & REDay.new(11,30,14,00)
58
+
59
+
60
+ What we need at this point is a way to write queries against these expressions
61
+ to determine whether we need to send a reminder. For this purpose, we can use
62
+ a Schedule and an associated Event, both of which are supplied by Runt.
63
+
64
+ schedule = Schedule.new
65
+
66
+ A Schedule holds zero or more Event/TemporalExpression pairs, allowing clients
67
+ to easily query and update TemporalExpressions as well perform certain range
68
+ operations as we will see in a moment. We'll create two events, one for each
69
+ side of the street:
70
+
71
+ north_event = Event.new("north side")
72
+
73
+ south_event = Event.new("south side")
74
+
75
+ Now we add each event and its associated occurrence to our Schedule:
76
+
77
+ schedule.add(north_event, north_expr)
78
+
79
+ schedule.add(south_event, south_expr)
80
+
81
+ An Event is simply a container for domain data. Although Runt uses Events
82
+ by default, Schedules will happily house any kind of Object. Internally, a
83
+ Schedule is really just a Hash where the keys are whatever it is you are
84
+ scheduling and the values are the TemporalExpressions you create.
85
+
86
+ class Schedule
87
+ ...
88
+
89
+ def add(obj, expression)
90
+ @elems[obj]=expression
91
+ end
92
+ ...
93
+
94
+ Now that we have a Schedule configured, we need something to check it and
95
+ then let us know if we need to move the car. For this, we'll create a simple
96
+ class called Reminder which will function as the "main-able" part of
97
+ our app.
98
+
99
+ We'll start by creating an easily testable constructor which will be passed
100
+ a Schedule instance (like the one we just created) and an SMTP server.
101
+
102
+
103
+ class Reminder
104
+
105
+ attr_reader :schedule, :mail_server
106
+
107
+ def initialize(schedule,mail_server)
108
+ @schedule = schedule
109
+ @mail_server = mail_server
110
+ end
111
+ ...
112
+
113
+ Being devoted Agilists, we'll of course also create a unit test to
114
+ help flesh out the specifics of our new Reminder class. We'll create
115
+ test fixtures using the Runt Objects described above.
116
+
117
+
118
+ class ReminderTest < Test::Unit::TestCase
119
+
120
+ include Runt
121
+
122
+ def setup
123
+ @schedule = Schedule.new
124
+ @north_event = Event.new("north side of the street will be ticketed")
125
+ north_expr = (DIWeek.new(Mon) | DIWeek.new(Wed) | DIWeek.new(Fri)) & REDay.new(8,00,11,00)
126
+ @schedule.add(@north_event, north_expr)
127
+ @south_event = Event.new("south side of the street will be ticketed")
128
+ south_expr = (DIWeek.new(Tue) | DIWeek.new(Thu)) & REDay.new(11,30,14,00)
129
+ @schedule.add(@south_event, south_expr)
130
+ @mail_server = MailServer.new
131
+ @reminder = Reminder.new(@schedule, @mail_server)
132
+ @saturday_at_10 = PDate.min(2007,11,24,10,0,0)
133
+ @monday_at_10 = PDate.min(2007,11,26,10,0,0)
134
+ @tuesday_at_noon = PDate.min(2007,11,27,12,0,0)
135
+ end
136
+
137
+ def test_initalize
138
+ assert_same @schedule, @reminder.schedule, "Expected #{@schedule} instead was #{@reminder.schedule}"
139
+ assert_same @mail_server, @reminder.mail_server, "Expected #{@mail_server} instead was #{@reminder.mail_server}"
140
+ end
141
+ ...
142
+
143
+ For the purposes of this tutorial, the mail server will simply be a stub to
144
+ illustrate how a real one might be used.
145
+
146
+ class MailServer
147
+
148
+ Struct.new("Email",:to,:from,:subject,:text)
149
+
150
+ def send(to, from, subject, text)
151
+ Struct::Email.new(to, from, subject, text)
152
+ # etc...
153
+ end
154
+
155
+ end
156
+
157
+ Next, let's add a method to our Reminder class which actually checks our
158
+ schedule using a date which is passed in as a parameter.
159
+
160
+ class Reminder
161
+ ...
162
+ def check(date)
163
+ return @schedule.events(date)
164
+ end
165
+ ...
166
+
167
+ The Schedule#events method will return an Array of Event Objects for any
168
+ events which occur at the date and time given by the method's argument. Usage
169
+ is easily demonstrated by a test case which makes use of the fixtures created
170
+ by the TestCase#setup method defined above.
171
+
172
+ class ReminderTest < Test::Unit::TestCase
173
+ ...
174
+ def test_check
175
+ assert_equal 1, @reminder.check(@monday_at_10).size, "Unexpected size #{@reminder.check(@monday_at_10).size} returned"
176
+ assert_same @north_event, @reminder.check(@monday_at_10)[0], "Expected Event #{@north_event}. Got #{@reminder.check(@monday_at_10)[0]}."
177
+ assert_equal 1, @reminder.check(@tuesday_at_noon).size, "Unexpected size #{@reminder.check(@tuesday_at_noon).size} returned"
178
+ assert_same @south_event, @reminder.check(@tuesday_at_noon)[0], "Expected Event #{@south_event}. Got #{@reminder.check(@tuesday_at_noon)[0]}."
179
+ assert @reminder.check(@saturday_at_10).empty?, "Expected empty Array. Got #{@reminder.check(@saturday_at_10)}"
180
+ end
181
+ ...
182
+
183
+
184
+ There are other methods in the Schedule API which allow a client to query for
185
+ information. Although we don't need them for this tutorial, I'll mention two
186
+ briefly because they are generally useful. The first is Schedule#dates
187
+ which will return an Array of PDate Objects which occur during the DateRange
188
+ supplied as a parameter. The second is Schedule#include? which returns a
189
+ boolean value indicating whether the Event occurs on the date which are both
190
+ supplied as arguments.
191
+
192
+ Next, let's make use of the mail server argument given to the Reminder class
193
+ in it's constructor. This is the method that will be called when a call to the
194
+ Reminder#check method produces results.
195
+
196
+ class Reminder
197
+ ...
198
+ def send(date)
199
+ text = "Warning: " + events.join(', ')
200
+ return @mail_server.send(TO, FROM, SUBJECT, text)
201
+ end
202
+ ...
203
+
204
+
205
+ Testing this is simple thanks to our MailServer stub which simply regurgitates
206
+ the text argument it's passed as a result.
207
+
208
+ class ReminderTest < Test::Unit::TestCase
209
+ ...
210
+ def test_send
211
+ params = [@north_event, @south_event]
212
+ result = @reminder.send(params)
213
+ assert_email result, Reminder::TEXT + params.join(', ')
214
+ end
215
+
216
+ def assert_email(result, text)
217
+ assert_equal Reminder::TO, result.to, "Unexpected value for 'to' field of Email Struct: #{result.to}"
218
+ assert_equal Reminder::FROM, result.from, "Unexpected value for 'from' field of Email Struct: #{result.from}"
219
+ assert_equal Reminder::SUBJECT, result.subject, "Unexpected value for 'subject' field of Email Struct: #{result.subject}"
220
+ assert_equal text, result.text, "Unexpected value for 'text' field of Email Struct: #{result.text}"
221
+ end
222
+ ...
223
+
224
+ Note the ReminderTest#assert_email method we've added to make assertions
225
+ common to multiple test cases.
226
+
227
+
228
+ Now, let's tie the whole thing together with a method which which checks for
229
+ occuring Events and (upon finding some) sends a reminder. This method is
230
+ really the only one in the Reminder class that needs to be public.
231
+
232
+ class Reminder
233
+ ...
234
+ def run(date)
235
+ result = self.check(date)
236
+ self.send(result) if !result.empty?
237
+ end
238
+ ...
239
+
240
+ class ReminderTest < Test::Unit::TestCase
241
+ ...
242
+ def test_send
243
+ params = [@north_event, @south_event]
244
+ result = @reminder.send(params)
245
+ assert_email result, Reminder::TEXT + params.join(', ')
246
+ end
247
+ ...
248
+
249
+ Finally, we'll cheat a bit and stitch every thing together so it can be run
250
+ from a command line.
251
+
252
+
253
+ include Runt
254
+
255
+ schedule = Schedule.new
256
+ north_event = Event.new("north side")
257
+ north_expr = (DIWeek.new(Mon) | DIWeek.new(Wed) | DIWeek.new(Fri)) & REDay.new(8,00,11,00)
258
+ schedule.add(north_event, north_expr)
259
+ south_event = Event.new("south side")
260
+ south_expr = (DIWeek.new(Tue) | DIWeek.new(Thu)) & REDay.new(11,30,14,00)
261
+ schedule.add(south_event, south_expr)
262
+ reminder = Reminder.new(schedule, MailServer.new)
263
+ while true
264
+ sleep 15.minutes
265
+ reminder.run Time.now
266
+ end
267
+
268
+ So, here's all the code for this tutorial (it's in the Runt distribution under
269
+ the examples folder):
270
+
271
+ ### schedule_tutorial.rb ###
272
+
273
+ #!/usr/bin/ruby
274
+
275
+ require 'runt'
276
+
277
+ class Reminder
278
+
279
+ TO = "me@myselfandi.com"
280
+ FROM = "reminder@daemon.net"
281
+ SUBJECT = "Move your car!"
282
+ TEXT = "Warning: "
283
+
284
+ attr_reader :schedule, :mail_server
285
+
286
+ def initialize(schedule,mail_server)
287
+ @schedule = schedule
288
+ @mail_server = mail_server
289
+ end
290
+ def run(date)
291
+ result = self.check(date)
292
+ self.send(result) if !result.empty?
293
+ end
294
+ def check(date)
295
+ puts "Checking the schedule..." if $DEBUG
296
+ return @schedule.events(date)
297
+ end
298
+ def send(events)
299
+ text = TEXT + events.join(', ')
300
+ return @mail_server.send(TO, FROM, SUBJECT, text)
301
+ end
302
+ end
303
+
304
+ class MailServer
305
+ Struct.new("Email",:to,:from,:subject,:text)
306
+ def send(to, from, subject, text)
307
+ puts "Sending message TO: #{to} FROM: #{from} RE: #{subject}..." if $DEBUG
308
+ Struct::Email.new(to, from, subject, text)
309
+ # etc...
310
+ end
311
+ end
312
+
313
+
314
+ if __FILE__ == $0
315
+
316
+ include Runt
317
+
318
+ schedule = Schedule.new
319
+ north_event = Event.new("north side")
320
+ north_expr = (DIWeek.new(Mon) | DIWeek.new(Wed) | DIWeek.new(Fri)) & REDay.new(8,00,11,00)
321
+ schedule.add(north_event, north_expr)
322
+ south_event = Event.new("south side")
323
+ south_expr = (DIWeek.new(Tue) | DIWeek.new(Thu)) & REDay.new(11,30,14,00)
324
+ schedule.add(south_event, south_expr)
325
+ reminder = Reminder.new(schedule, MailServer.new)
326
+ while true
327
+ sleep 15.minutes
328
+ reminder.run Time.now
329
+ end
330
+
331
+ end
332
+
333
+
334
+ ### schedule_tutorialtest.rb ###
335
+
336
+ #!/usr/bin/ruby
337
+
338
+ require 'test/unit'
339
+ require 'runt'
340
+ require 'schedule_tutorial'
341
+
342
+ class ReminderTest < Test::Unit::TestCase
343
+
344
+ include Runt
345
+
346
+ def setup
347
+ @schedule = Schedule.new
348
+ @north_event = Event.new("north side of the street will be ticketed")
349
+ north_expr = (DIWeek.new(Mon) | DIWeek.new(Wed) | DIWeek.new(Fri)) & REDay.new(8,00,11,00)
350
+ @schedule.add(@north_event, north_expr)
351
+ @south_event = Event.new("south side of the street will be ticketed")
352
+ south_expr = (DIWeek.new(Tue) | DIWeek.new(Thu)) & REDay.new(11,30,14,00)
353
+ @schedule.add(@south_event, south_expr)
354
+ @mail_server = MailServer.new
355
+ @reminder = Reminder.new(@schedule, @mail_server)
356
+ @saturday_at_10 = PDate.min(2007,11,24,10,0,0)
357
+ @monday_at_10 = PDate.min(2007,11,26,10,0,0)
358
+ @tuesday_at_noon = PDate.min(2007,11,27,12,0,0)
359
+ end
360
+ def test_initalize
361
+ assert_same @schedule, @reminder.schedule, "Expected #{@schedule} instead was #{@reminder.schedule}"
362
+ assert_same @mail_server, @reminder.mail_server, "Expected #{@mail_server} instead was #{@reminder.mail_server}"
363
+ end
364
+ def test_send
365
+ params = [@north_event, @south_event]
366
+ result = @reminder.send(params)
367
+ assert_email result, Reminder::TEXT + params.join(', ')
368
+ end
369
+ def test_check
370
+ assert_equal 1, @reminder.check(@monday_at_10).size, "Unexpected size #{@reminder.check(@monday_at_10).size} returned"
371
+ assert_same @north_event, @reminder.check(@monday_at_10)[0], "Expected Event #{@north_event}. Got #{@reminder.check(@monday_at_10)[0]}."
372
+ assert_equal 1, @reminder.check(@tuesday_at_noon).size, "Unexpected size #{@reminder.check(@tuesday_at_noon).size} returned"
373
+ assert_same @south_event, @reminder.check(@tuesday_at_noon)[0], "Expected Event #{@south_event}. Got #{@reminder.check(@tuesday_at_noon)[0]}."
374
+ assert @reminder.check(@saturday_at_10).empty?, "Expected empty Array. Got #{@reminder.check(@saturday_at_10)}"
375
+ end
376
+ def test_run
377
+ result = @reminder.run(@monday_at_10)
378
+ assert_email result, Reminder::TEXT + @north_event.to_s
379
+ end
380
+ def assert_email(result, text)
381
+ assert_equal Reminder::TO, result.to, "Unexpected value for 'to' field of Email Struct: #{result.to}"
382
+ assert_equal Reminder::FROM, result.from, "Unexpected value for 'from' field of Email Struct: #{result.from}"
383
+ assert_equal Reminder::SUBJECT, result.subject, "Unexpected value for 'subject' field of Email Struct: #{result.subject}"
384
+ assert_equal text, result.text, "Unexpected value for 'text' field of Email Struct: #{result.text}"
385
+ end
386
+ end
387
+
388
+
389
+ <em>See Also:</em>
390
+
391
+ * Fowler's recurring event pattern[http://martinfowler.com/apsupp/recurring.pdf]
392
+
393
+ * Other temporal patterns[http://martinfowler.com/ap2/timeNarrative.html]