director 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +21 -0
- data/Gemfile +4 -0
- data/Guardfile +10 -0
- data/LICENSE +22 -0
- data/README.md +126 -0
- data/Rakefile +2 -0
- data/bin/director +3 -0
- data/director.gemspec +24 -0
- data/lib/director.rb +11 -0
- data/lib/director/config_loader.rb +42 -0
- data/lib/director/event.rb +5 -0
- data/lib/director/event_repository.rb +21 -0
- data/lib/director/query.rb +16 -0
- data/lib/director/query_resolver.rb +30 -0
- data/lib/director/schedule.rb +27 -0
- data/lib/director/trigger.rb +34 -0
- data/lib/director/version.rb +3 -0
- data/spec/config_loader_spec.rb +57 -0
- data/spec/event_repository_spec.rb +64 -0
- data/spec/fixtures/simple_config.yaml +4 -0
- data/spec/fixtures/timed_config.yaml +13 -0
- data/spec/query_resolver_spec.rb +63 -0
- data/spec/query_spec.rb +13 -0
- data/spec/scheduler_spec.rb +88 -0
- metadata +164 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
guard 'rspec', :version => 2 do
|
2
|
+
watch(%r{^spec/.+_spec\.rb$})
|
3
|
+
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
|
4
|
+
watch('spec/spec_helper.rb') { "spec" }
|
5
|
+
|
6
|
+
watch(%r{^lib/director/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
7
|
+
watch(%r{^lib/(.+)\.rb$}) { "spec" }
|
8
|
+
watch(%r{^spec/support/(.+)\.rb$}) { "spec" }
|
9
|
+
end
|
10
|
+
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Jacob Atzen
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
# Director
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'director'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install director
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
The first thing we need to do to start using the scheduler is to get our hands
|
22
|
+
on a schedule object:
|
23
|
+
|
24
|
+
schedule = Director::Schedule.new(event_repository)
|
25
|
+
|
26
|
+
Notice that you need to pass an `event_repository` object to the Schedule. See
|
27
|
+
the section on Event Repository for details.
|
28
|
+
|
29
|
+
Then we need to add a trigger to the schedule:
|
30
|
+
|
31
|
+
schedule.add_trigger(:invitation,
|
32
|
+
:event => :participant_join,
|
33
|
+
:action => :send_invite)
|
34
|
+
|
35
|
+
Then we need to tell the schedule which handlers are to be used for handling
|
36
|
+
which actions. Say we have an InviteSender module like this:
|
37
|
+
|
38
|
+
module InviteSender
|
39
|
+
extend self
|
40
|
+
def call(participant_id)
|
41
|
+
# Do something
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
Now let's tell the scheduler to use the InviteSender to handle send\_invite
|
46
|
+
actions:
|
47
|
+
|
48
|
+
schedule.add_handler(:send_invite, InviteSender)
|
49
|
+
|
50
|
+
And finally we'll notify the scheduler that a participant is to join the
|
51
|
+
schedule:
|
52
|
+
|
53
|
+
participant_id = 42
|
54
|
+
schedule.notify(participant_id, :participant_join)
|
55
|
+
|
56
|
+
Now we're ready to execute the schedule:
|
57
|
+
|
58
|
+
schedule.execute
|
59
|
+
|
60
|
+
Which in turn will call:
|
61
|
+
|
62
|
+
InviteSender.call(42)
|
63
|
+
|
64
|
+
## Delayed triggers
|
65
|
+
|
66
|
+
Sometimes you want to schedule a trigger sometime after an event. This can be
|
67
|
+
done with:
|
68
|
+
|
69
|
+
schedule.add_trigger(:reminder,
|
70
|
+
:event => 'participant_join'
|
71
|
+
:offset => 3600,
|
72
|
+
:action => :send_reminder)
|
73
|
+
|
74
|
+
Where the offset is some number of seconds that must pass after the event
|
75
|
+
before the trigger is run.
|
76
|
+
|
77
|
+
## Event Repository
|
78
|
+
|
79
|
+
To use the scheduler you need to provide an event repository object. This
|
80
|
+
object needs to respond to `notify(participant_id, event_name)` and
|
81
|
+
`search(criteria)`.
|
82
|
+
|
83
|
+
`participant_id` will most likely be an integer and `event_name` must be a
|
84
|
+
string.
|
85
|
+
|
86
|
+
For the search the criteria is an array of hashes that look like:
|
87
|
+
|
88
|
+
{ :name => 'event_name', :includes => true }
|
89
|
+
|
90
|
+
This means that the event `event_name` must have happened to the participant.
|
91
|
+
The inverse is also possible:
|
92
|
+
|
93
|
+
{ :name => 'event_name', :excludes => true }
|
94
|
+
|
95
|
+
Meaning that the event `event_name` must not have happened to the participant.
|
96
|
+
The search method must return an array of participant ids that match the
|
97
|
+
criteria.
|
98
|
+
|
99
|
+
Optionally the hash might include a `before` key if the event must have
|
100
|
+
happened before a given time.
|
101
|
+
|
102
|
+
{ :name => 'event_name', :includes => true, :before => [time object] }
|
103
|
+
|
104
|
+
## Config files
|
105
|
+
|
106
|
+
The schedule can be instantiated from a YAML file. The most simple
|
107
|
+
example is a schedule that basically does nothing but run an action when it's
|
108
|
+
told to:
|
109
|
+
|
110
|
+
triggers:
|
111
|
+
invitation:
|
112
|
+
event: participant_join
|
113
|
+
action: send_invite
|
114
|
+
offset: 1 day
|
115
|
+
|
116
|
+
This schedule can be loaded with:
|
117
|
+
|
118
|
+
schedule = Director::ConfigLoader.load_schedule(YAML::load_file('path/to/file'))
|
119
|
+
|
120
|
+
## Contributing
|
121
|
+
|
122
|
+
1. Fork it
|
123
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
124
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
125
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
126
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/bin/director
ADDED
data/director.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/director/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Jacob Atzen"]
|
6
|
+
gem.email = ["jacob@incremental.dk"]
|
7
|
+
gem.description = %q{Scheduler}
|
8
|
+
gem.summary = %q{Scheduler gem}
|
9
|
+
gem.homepage = ""
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\).reject{|f| f.match(/\Aschedule_demo/) }
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "director"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Director::VERSION
|
17
|
+
|
18
|
+
gem.add_runtime_dependency 'activesupport'
|
19
|
+
|
20
|
+
gem.add_development_dependency 'rake'
|
21
|
+
gem.add_development_dependency 'rspec', "~> 2.8.0"
|
22
|
+
gem.add_development_dependency 'guard-rspec'
|
23
|
+
gem.add_development_dependency 'growl'
|
24
|
+
end
|
data/lib/director.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require "director/config_loader"
|
2
|
+
require "director/event"
|
3
|
+
require "director/event_repository"
|
4
|
+
require "director/schedule"
|
5
|
+
require "director/trigger"
|
6
|
+
require "director/query"
|
7
|
+
require "director/version"
|
8
|
+
|
9
|
+
module Director
|
10
|
+
# Your code goes here...
|
11
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Director
|
4
|
+
class ConfigLoader
|
5
|
+
def self.load_schedule(schedule, filename)
|
6
|
+
new(schedule).load_config(YAML::load_file(filename))
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(schedule)
|
10
|
+
@schedule = schedule
|
11
|
+
end
|
12
|
+
|
13
|
+
def load_config(config)
|
14
|
+
config['triggers'].each do |name, trigger_config|
|
15
|
+
if trigger_config['offset']
|
16
|
+
trigger_config['offset'] = parse_time(trigger_config['offset'])
|
17
|
+
end
|
18
|
+
@schedule.add_trigger(name, trigger_config)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def parse_time(time_string)
|
23
|
+
md = time_string.match(/([0-9]+)\s+([a-z]+)/)
|
24
|
+
count = md[1]
|
25
|
+
unit = md[2]
|
26
|
+
unit_to_i(unit) * count.to_i
|
27
|
+
end
|
28
|
+
|
29
|
+
def unit_to_i(unit)
|
30
|
+
case unit
|
31
|
+
when /month/
|
32
|
+
30 * 24 * 60 * 60
|
33
|
+
when /day/
|
34
|
+
24 * 60 * 60
|
35
|
+
when /hour/
|
36
|
+
60 * 60
|
37
|
+
when /minute/
|
38
|
+
60
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Director
|
2
|
+
class EventRepository
|
3
|
+
def initialize
|
4
|
+
@participants = Hash.new{|hash, key| hash[key] = []}
|
5
|
+
end
|
6
|
+
|
7
|
+
def notify(participant_id, event_name, created_at = Time.now)
|
8
|
+
event = Event.new
|
9
|
+
event.name = event_name
|
10
|
+
event.created_at = created_at
|
11
|
+
@participants[participant_id] << event
|
12
|
+
end
|
13
|
+
|
14
|
+
def search(criteria)
|
15
|
+
resolver = Director::QueryResolver.new(criteria)
|
16
|
+
@participants.select do |participant, events|
|
17
|
+
resolver.match?(events)
|
18
|
+
end.map{|array| array[0]}
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Director
|
2
|
+
class Query
|
3
|
+
attr_reader :criteria
|
4
|
+
def initialize
|
5
|
+
@criteria = []
|
6
|
+
end
|
7
|
+
|
8
|
+
def includes_event(event, opts = {})
|
9
|
+
@criteria << { :name => event, :includes => true }.merge(opts)
|
10
|
+
end
|
11
|
+
|
12
|
+
def excludes_event(event, opts = {})
|
13
|
+
@criteria << { :name => event, :excludes => true }.merge(opts)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Director
|
2
|
+
class QueryResolver
|
3
|
+
attr_reader :criteria
|
4
|
+
def initialize(criteria)
|
5
|
+
@criteria = criteria
|
6
|
+
end
|
7
|
+
|
8
|
+
def match?(events)
|
9
|
+
criteria.all?{|criterion| passes?(events, criterion) }
|
10
|
+
end
|
11
|
+
|
12
|
+
def passes?(events, criterion)
|
13
|
+
event = find_event(events, criterion)
|
14
|
+
result = !event.nil? if(criterion[:includes])
|
15
|
+
result = event.nil? if(criterion[:excludes])
|
16
|
+
result
|
17
|
+
end
|
18
|
+
|
19
|
+
def find_event(events, criterion)
|
20
|
+
events.detect do |event|
|
21
|
+
if(criterion[:before])
|
22
|
+
event.name == criterion[:name] &&
|
23
|
+
event.created_at < criterion[:before]
|
24
|
+
else
|
25
|
+
event.name == criterion[:name]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Director
|
2
|
+
class Schedule
|
3
|
+
def initialize(event_repository)
|
4
|
+
@event_repository = event_repository
|
5
|
+
@triggers = []
|
6
|
+
@handlers = {}
|
7
|
+
end
|
8
|
+
|
9
|
+
def add_trigger(name, opts)
|
10
|
+
@triggers << Trigger.new(name, opts)
|
11
|
+
end
|
12
|
+
|
13
|
+
def add_handler(action, handler)
|
14
|
+
@handlers[action.to_s] = handler
|
15
|
+
end
|
16
|
+
|
17
|
+
def notify(participant_id, event_name)
|
18
|
+
@event_repository.notify(participant_id, event_name)
|
19
|
+
end
|
20
|
+
|
21
|
+
def execute
|
22
|
+
@triggers.each do |trigger|
|
23
|
+
trigger.run(@event_repository, @handlers[trigger.action])
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'active_support/core_ext/object'
|
2
|
+
|
3
|
+
module Director
|
4
|
+
class Trigger
|
5
|
+
attr_reader :action
|
6
|
+
def initialize(name, opts)
|
7
|
+
opts.symbolize_keys!
|
8
|
+
@name, @event, @action = name, opts[:event].to_s, opts[:action].to_s
|
9
|
+
@offset = opts[:offset]
|
10
|
+
end
|
11
|
+
|
12
|
+
def run(event_repository, handler)
|
13
|
+
event_repository.search(query.criteria).each do |participant_id|
|
14
|
+
handler.call(participant_id)
|
15
|
+
event_repository.notify(participant_id, triggered_event_name)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def query
|
20
|
+
query = Query.new
|
21
|
+
opts = {}
|
22
|
+
if @offset
|
23
|
+
opts = { :before => Time.now - @offset }
|
24
|
+
end
|
25
|
+
query.includes_event(@event, opts)
|
26
|
+
query.excludes_event(triggered_event_name)
|
27
|
+
query
|
28
|
+
end
|
29
|
+
|
30
|
+
def triggered_event_name
|
31
|
+
"__handled_#{@name}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'director/config_loader'
|
2
|
+
|
3
|
+
describe Director::ConfigLoader do
|
4
|
+
let(:schedule) { stub('Schedule') }
|
5
|
+
|
6
|
+
it "loads a simple config" do
|
7
|
+
schedule.should_receive(:add_trigger).with(
|
8
|
+
'invitation',
|
9
|
+
{
|
10
|
+
'event' => 'participant_join',
|
11
|
+
'action' => 'send_invite'
|
12
|
+
}
|
13
|
+
)
|
14
|
+
Director::ConfigLoader.load_schedule(schedule, 'spec/fixtures/simple_config.yaml')
|
15
|
+
end
|
16
|
+
|
17
|
+
context "with a timed config" do
|
18
|
+
before(:each) { schedule.stub(:add_trigger) }
|
19
|
+
|
20
|
+
it "loads the invitation" do
|
21
|
+
schedule.should_receive(:add_trigger).with(
|
22
|
+
'invitation',
|
23
|
+
{
|
24
|
+
'event' => 'participant_join',
|
25
|
+
'action' => 'send_invite',
|
26
|
+
'offset' => 3600
|
27
|
+
}
|
28
|
+
)
|
29
|
+
Director::ConfigLoader.load_schedule(schedule, 'spec/fixtures/timed_config.yaml')
|
30
|
+
end
|
31
|
+
|
32
|
+
it "loads the reminder" do
|
33
|
+
schedule.should_receive(:add_trigger).with(
|
34
|
+
'reminder',
|
35
|
+
{
|
36
|
+
'event' => 'participant_join',
|
37
|
+
'action' => 'send_invite',
|
38
|
+
'offset' => 2 * 24 * 60 * 60
|
39
|
+
}
|
40
|
+
)
|
41
|
+
Director::ConfigLoader.load_schedule(schedule, 'spec/fixtures/timed_config.yaml')
|
42
|
+
end
|
43
|
+
|
44
|
+
it "loads the final" do
|
45
|
+
schedule.should_receive(:add_trigger).with(
|
46
|
+
'final',
|
47
|
+
{
|
48
|
+
'event' => 'participant_join',
|
49
|
+
'action' => 'send_invite',
|
50
|
+
'offset' => 2 * 30 * 24 * 60 * 60
|
51
|
+
}
|
52
|
+
)
|
53
|
+
Director::ConfigLoader.load_schedule(schedule, 'spec/fixtures/timed_config.yaml')
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'director/query'
|
2
|
+
require 'director/query_resolver'
|
3
|
+
require 'director/event_repository'
|
4
|
+
|
5
|
+
describe Director::EventRepository do
|
6
|
+
let(:repos) { Director::EventRepository.new }
|
7
|
+
let(:participant_1) { stub('Participant 1') }
|
8
|
+
let(:participant_2) { stub('Participant 2') }
|
9
|
+
let(:event1) { 'event1' }
|
10
|
+
let(:event2) { 'event2' }
|
11
|
+
let(:event_yesterday) { 'event_yesterday' }
|
12
|
+
let(:now) { Time.now }
|
13
|
+
let(:yesterday) { now - (24 * 60 * 60) }
|
14
|
+
let(:two_days_ago) { now - (2 * 24 * 60 * 60) }
|
15
|
+
|
16
|
+
context "two participants with event1 where one also has event2" do
|
17
|
+
before(:each) do
|
18
|
+
repos.notify(participant_1, event1)
|
19
|
+
repos.notify(participant_1, event2)
|
20
|
+
repos.notify(participant_2, event1)
|
21
|
+
end
|
22
|
+
|
23
|
+
it "finds both participants when asked for participants with the event" do
|
24
|
+
query = Director::Query.new
|
25
|
+
query.includes_event(event1)
|
26
|
+
repos.search(query.criteria).should have(2).items
|
27
|
+
end
|
28
|
+
|
29
|
+
it "finds only the participant without event2" do
|
30
|
+
query = Director::Query.new
|
31
|
+
query.includes_event(event1)
|
32
|
+
query.excludes_event(event2)
|
33
|
+
repos.search(query.criteria).should == [participant_2]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context "one participant with an event and one without" do
|
38
|
+
before(:each) do
|
39
|
+
repos.notify(participant_1, event2)
|
40
|
+
repos.notify(participant_2, event1)
|
41
|
+
end
|
42
|
+
it "finds only the participant with the event" do
|
43
|
+
query = Director::Query.new
|
44
|
+
query.includes_event(event1)
|
45
|
+
repos.search(query.criteria).should == [participant_2]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
context "a participant with an event yesterday" do
|
50
|
+
before(:each) do
|
51
|
+
repos.notify(participant_1, event_yesterday, yesterday)
|
52
|
+
end
|
53
|
+
it "finds the events from yesterday" do
|
54
|
+
query = Director::Query.new
|
55
|
+
query.includes_event(event_yesterday, :before => now)
|
56
|
+
repos.search(query.criteria).should == [participant_1]
|
57
|
+
end
|
58
|
+
it "skips the event when asked for 2 days ago" do
|
59
|
+
query = Director::Query.new
|
60
|
+
query.includes_event(event_yesterday, :before => two_days_ago)
|
61
|
+
repos.search(query.criteria).should be_empty
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'director/query'
|
2
|
+
require 'director/query_resolver'
|
3
|
+
|
4
|
+
describe Director::QueryResolver do
|
5
|
+
let(:query) { Director::Query.new }
|
6
|
+
let(:query_resolver) { Director::QueryResolver.new(query.criteria) }
|
7
|
+
let(:time) { Time.now }
|
8
|
+
let(:event1) { stub(:name => 'event1', :created_at => time) }
|
9
|
+
let(:event2) { stub(:name => 'event2', :created_at => time) }
|
10
|
+
let(:event3) { stub(:name => 'event3', :created_at => time) }
|
11
|
+
|
12
|
+
it "finds an included event" do
|
13
|
+
query.includes_event('event1')
|
14
|
+
query_resolver.should be_match([event1])
|
15
|
+
end
|
16
|
+
|
17
|
+
it "skips an excluded event" do
|
18
|
+
query.excludes_event('event1')
|
19
|
+
query_resolver.should_not be_match([event1])
|
20
|
+
end
|
21
|
+
|
22
|
+
context "a query with two includes" do
|
23
|
+
before(:each) do
|
24
|
+
query.includes_event('event1')
|
25
|
+
query.includes_event('event2')
|
26
|
+
end
|
27
|
+
it "should match a set of events" do
|
28
|
+
query_resolver.should be_match([event1, event3, event2])
|
29
|
+
end
|
30
|
+
it "does not match when an event is missing" do
|
31
|
+
query_resolver.should_not be_match([event1, event3])
|
32
|
+
end
|
33
|
+
end
|
34
|
+
context "a query with an includes with time before now" do
|
35
|
+
before(:each) do
|
36
|
+
query.includes_event('event1', :before => time - 1)
|
37
|
+
end
|
38
|
+
it "does not match when an event is missing" do
|
39
|
+
query_resolver.should_not be_match([event1])
|
40
|
+
end
|
41
|
+
end
|
42
|
+
context "a query with an includes with time after now" do
|
43
|
+
before(:each) do
|
44
|
+
query.includes_event('event1', :before => time + 1)
|
45
|
+
end
|
46
|
+
it "does not match when an event is missing" do
|
47
|
+
query_resolver.should be_match([event1])
|
48
|
+
end
|
49
|
+
end
|
50
|
+
context "a query with an include and an exclude" do
|
51
|
+
before(:each) do
|
52
|
+
query.includes_event('event1')
|
53
|
+
query.excludes_event('event2')
|
54
|
+
end
|
55
|
+
it "matches a set of events" do
|
56
|
+
query_resolver.should be_match([event1, event3])
|
57
|
+
end
|
58
|
+
it "does not match a set of events" do
|
59
|
+
query_resolver.should_not be_match([event1, event2, event3])
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
data/spec/query_spec.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'director/query'
|
2
|
+
|
3
|
+
describe Director::Query do
|
4
|
+
subject { Director::Query.new }
|
5
|
+
before(:each) do
|
6
|
+
subject.includes_event('event1')
|
7
|
+
subject.includes_event('event2')
|
8
|
+
subject.excludes_event('event3')
|
9
|
+
end
|
10
|
+
its(:criteria) { should include({:name => 'event1', :includes => true}) }
|
11
|
+
its(:criteria) { should include({:name => 'event2', :includes => true}) }
|
12
|
+
its(:criteria) { should include({:name => 'event3', :excludes => true}) }
|
13
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'director'
|
2
|
+
|
3
|
+
describe Director::Schedule do
|
4
|
+
subject { Director::Schedule.new(Director::EventRepository.new) }
|
5
|
+
let(:some_event) { 'some_event' }
|
6
|
+
let(:some_other_event) { 'some_other_event' }
|
7
|
+
context 'with one trigger' do
|
8
|
+
let(:handler) { stub }
|
9
|
+
|
10
|
+
before(:each) do
|
11
|
+
subject.add_trigger(:now, 'event' => 'some_event', :action => :some_action)
|
12
|
+
subject.add_handler(:some_action, handler)
|
13
|
+
end
|
14
|
+
|
15
|
+
it "can execute an action for two participants" do
|
16
|
+
handler.should_receive(:call).with(42)
|
17
|
+
handler.should_receive(:call).with(43)
|
18
|
+
subject.notify(42, some_event)
|
19
|
+
subject.notify(43, some_event)
|
20
|
+
subject.execute
|
21
|
+
end
|
22
|
+
|
23
|
+
it "only executes an action once" do
|
24
|
+
handler.should_receive(:call).with(42)
|
25
|
+
subject.notify(42, some_event)
|
26
|
+
subject.execute
|
27
|
+
subject.execute
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
context 'with two triggers' do
|
32
|
+
let(:handler1) { stub }
|
33
|
+
let(:handler2) { stub }
|
34
|
+
|
35
|
+
before(:each) do
|
36
|
+
subject.add_trigger(:now,
|
37
|
+
:event => 'some_event',
|
38
|
+
:action => :some_action)
|
39
|
+
subject.add_handler(:some_action, handler1)
|
40
|
+
subject.add_trigger(:again,
|
41
|
+
:event => 'some_other_event',
|
42
|
+
:action => :some_other_action)
|
43
|
+
subject.add_handler(:some_other_action, handler2)
|
44
|
+
end
|
45
|
+
|
46
|
+
it "can execute an action for two participants" do
|
47
|
+
handler1.should_receive(:call).with(42)
|
48
|
+
handler1.should_receive(:call).with(43)
|
49
|
+
subject.notify(42, some_event)
|
50
|
+
subject.notify(43, some_event)
|
51
|
+
subject.execute
|
52
|
+
end
|
53
|
+
|
54
|
+
it "only executes an action once" do
|
55
|
+
handler1.should_receive(:call).with(42)
|
56
|
+
subject.notify(42, some_event)
|
57
|
+
subject.execute
|
58
|
+
subject.execute
|
59
|
+
end
|
60
|
+
|
61
|
+
it "executes two actions" do
|
62
|
+
handler1.should_receive(:call).with(42)
|
63
|
+
handler2.should_receive(:call).with(42)
|
64
|
+
subject.notify(42, some_event)
|
65
|
+
subject.notify(42, some_other_event)
|
66
|
+
subject.execute
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
context 'with a timed trigger' do
|
71
|
+
let(:handler1) { stub }
|
72
|
+
let(:handler2) { stub }
|
73
|
+
|
74
|
+
before(:each) do
|
75
|
+
subject.add_trigger(:now,
|
76
|
+
:event => 'some_event',
|
77
|
+
:action => :some_action,
|
78
|
+
:offset => 60)
|
79
|
+
subject.add_handler(:some_action, handler1)
|
80
|
+
end
|
81
|
+
|
82
|
+
it "can execute an action for two participants" do
|
83
|
+
handler1.should_not_receive(:call).with(42)
|
84
|
+
subject.notify(42, some_event)
|
85
|
+
subject.execute
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
metadata
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: director
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jacob Atzen
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-06-19 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: activesupport
|
16
|
+
requirement: &2151800720 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *2151800720
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: rake
|
27
|
+
requirement: &2151800140 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *2151800140
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: rspec
|
38
|
+
requirement: &2151799160 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 2.8.0
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *2151799160
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: guard-rspec
|
49
|
+
requirement: &2151798240 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *2151798240
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: growl
|
60
|
+
requirement: &2151797260 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
type: :development
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *2151797260
|
69
|
+
description: Scheduler
|
70
|
+
email:
|
71
|
+
- jacob@incremental.dk
|
72
|
+
executables:
|
73
|
+
- !binary |-
|
74
|
+
ZGlyZWN0b3I=
|
75
|
+
extensions: []
|
76
|
+
extra_rdoc_files: []
|
77
|
+
files:
|
78
|
+
- !binary |-
|
79
|
+
LmdpdGlnbm9yZQ==
|
80
|
+
- !binary |-
|
81
|
+
R2VtZmlsZQ==
|
82
|
+
- !binary |-
|
83
|
+
R3VhcmRmaWxl
|
84
|
+
- !binary |-
|
85
|
+
TElDRU5TRQ==
|
86
|
+
- !binary |-
|
87
|
+
UkVBRE1FLm1k
|
88
|
+
- !binary |-
|
89
|
+
UmFrZWZpbGU=
|
90
|
+
- !binary |-
|
91
|
+
YmluL2RpcmVjdG9y
|
92
|
+
- !binary |-
|
93
|
+
ZGlyZWN0b3IuZ2Vtc3BlYw==
|
94
|
+
- !binary |-
|
95
|
+
bGliL2RpcmVjdG9yLnJi
|
96
|
+
- !binary |-
|
97
|
+
bGliL2RpcmVjdG9yL2NvbmZpZ19sb2FkZXIucmI=
|
98
|
+
- !binary |-
|
99
|
+
bGliL2RpcmVjdG9yL2V2ZW50LnJi
|
100
|
+
- !binary |-
|
101
|
+
bGliL2RpcmVjdG9yL2V2ZW50X3JlcG9zaXRvcnkucmI=
|
102
|
+
- !binary |-
|
103
|
+
bGliL2RpcmVjdG9yL3F1ZXJ5LnJi
|
104
|
+
- !binary |-
|
105
|
+
bGliL2RpcmVjdG9yL3F1ZXJ5X3Jlc29sdmVyLnJi
|
106
|
+
- !binary |-
|
107
|
+
bGliL2RpcmVjdG9yL3NjaGVkdWxlLnJi
|
108
|
+
- !binary |-
|
109
|
+
bGliL2RpcmVjdG9yL3RyaWdnZXIucmI=
|
110
|
+
- !binary |-
|
111
|
+
bGliL2RpcmVjdG9yL3ZlcnNpb24ucmI=
|
112
|
+
- !binary |-
|
113
|
+
c3BlYy9jb25maWdfbG9hZGVyX3NwZWMucmI=
|
114
|
+
- !binary |-
|
115
|
+
c3BlYy9ldmVudF9yZXBvc2l0b3J5X3NwZWMucmI=
|
116
|
+
- !binary |-
|
117
|
+
c3BlYy9maXh0dXJlcy9zaW1wbGVfY29uZmlnLnlhbWw=
|
118
|
+
- !binary |-
|
119
|
+
c3BlYy9maXh0dXJlcy90aW1lZF9jb25maWcueWFtbA==
|
120
|
+
- !binary |-
|
121
|
+
c3BlYy9xdWVyeV9yZXNvbHZlcl9zcGVjLnJi
|
122
|
+
- !binary |-
|
123
|
+
c3BlYy9xdWVyeV9zcGVjLnJi
|
124
|
+
- !binary |-
|
125
|
+
c3BlYy9zY2hlZHVsZXJfc3BlYy5yYg==
|
126
|
+
homepage: ''
|
127
|
+
licenses: []
|
128
|
+
post_install_message:
|
129
|
+
rdoc_options: []
|
130
|
+
require_paths:
|
131
|
+
- lib
|
132
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
133
|
+
none: false
|
134
|
+
requirements:
|
135
|
+
- - ! '>='
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '0'
|
138
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
139
|
+
none: false
|
140
|
+
requirements:
|
141
|
+
- - ! '>='
|
142
|
+
- !ruby/object:Gem::Version
|
143
|
+
version: '0'
|
144
|
+
requirements: []
|
145
|
+
rubyforge_project:
|
146
|
+
rubygems_version: 1.8.17
|
147
|
+
signing_key:
|
148
|
+
specification_version: 3
|
149
|
+
summary: Scheduler gem
|
150
|
+
test_files:
|
151
|
+
- !binary |-
|
152
|
+
c3BlYy9jb25maWdfbG9hZGVyX3NwZWMucmI=
|
153
|
+
- !binary |-
|
154
|
+
c3BlYy9ldmVudF9yZXBvc2l0b3J5X3NwZWMucmI=
|
155
|
+
- !binary |-
|
156
|
+
c3BlYy9maXh0dXJlcy9zaW1wbGVfY29uZmlnLnlhbWw=
|
157
|
+
- !binary |-
|
158
|
+
c3BlYy9maXh0dXJlcy90aW1lZF9jb25maWcueWFtbA==
|
159
|
+
- !binary |-
|
160
|
+
c3BlYy9xdWVyeV9yZXNvbHZlcl9zcGVjLnJi
|
161
|
+
- !binary |-
|
162
|
+
c3BlYy9xdWVyeV9zcGVjLnJi
|
163
|
+
- !binary |-
|
164
|
+
c3BlYy9zY2hlZHVsZXJfc3BlYy5yYg==
|