director 0.0.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.
- 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==
|