observatory 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/Gemfile.lock +5 -5
- data/Rakefile +3 -3
- data/lib/observatory/dispatcher.rb +111 -37
- data/lib/observatory/version.rb +1 -1
- data/observatory.gemspec +4 -4
- data/test/dispatcher_test.rb +45 -14
- metadata +26 -26
data/.gitignore
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
observatory (0.0
|
4
|
+
observatory (0.1.0)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: http://rubygems.org/
|
8
8
|
specs:
|
9
9
|
bluecloth (2.1.0)
|
10
|
-
rake (0.
|
11
|
-
yard (0.
|
10
|
+
rake (0.9.0)
|
11
|
+
yard (0.7.1)
|
12
12
|
|
13
13
|
PLATFORMS
|
14
14
|
ruby
|
@@ -16,5 +16,5 @@ PLATFORMS
|
|
16
16
|
DEPENDENCIES
|
17
17
|
bluecloth (~> 2.1)
|
18
18
|
observatory!
|
19
|
-
rake (~> 0.
|
20
|
-
yard (~> 0.
|
19
|
+
rake (~> 0.9)
|
20
|
+
yard (~> 0.7)
|
data/Rakefile
CHANGED
@@ -12,7 +12,7 @@ Rake::TestTask.new do |t|
|
|
12
12
|
t.verbose = true
|
13
13
|
end
|
14
14
|
|
15
|
-
YARD::Rake::YardocTask.new do |t|
|
16
|
-
t.files = ['lib/**/*.rb', 'app/**/*.rb', '-', 'LICENSE', 'HISTORY']
|
15
|
+
YARD::Rake::YardocTask.new do |t|
|
16
|
+
t.files = ['lib/**/*.rb', 'app/**/*.rb', '-', 'LICENSE', 'HISTORY']
|
17
17
|
t.options = %w{--title Observatory -m markdown}
|
18
|
-
end
|
18
|
+
end
|
@@ -1,33 +1,38 @@
|
|
1
1
|
module Observatory
|
2
2
|
# The Dispatcher is the central repository of all registered observers, and
|
3
3
|
# is used by observables to send out signals to these observers.
|
4
|
-
#
|
4
|
+
#
|
5
5
|
# A dispatcher is not a singleton object, which means you may very well have
|
6
6
|
# several dispatcher objects in your program, keeping track of different
|
7
7
|
# stacks of observers and observables. Note that this requires you to pass
|
8
8
|
# your dispatcher object around using dependency injection.
|
9
|
-
#
|
9
|
+
#
|
10
10
|
# ## For observables
|
11
|
-
#
|
11
|
+
#
|
12
12
|
# The stack of observers for any given signal is kept in {#observers}. When
|
13
13
|
# using {#notify}, {#notify_until} or {#filter} all observers in the stack
|
14
14
|
# will be called.
|
15
|
-
#
|
15
|
+
#
|
16
16
|
# ### Notification methods
|
17
|
-
#
|
17
|
+
#
|
18
18
|
# Observable objects may use the following methods to trigger their
|
19
19
|
# observers:
|
20
|
-
#
|
20
|
+
#
|
21
21
|
# * {#notify} to call all observers.
|
22
22
|
# * {#notify_until} to call observers until one stops the chain.
|
23
23
|
# * {#filter} to let all observers alter a given value.
|
24
|
-
#
|
24
|
+
#
|
25
25
|
# ## For observers
|
26
|
-
#
|
26
|
+
#
|
27
27
|
# An object that observes another object is an observer, and it can
|
28
28
|
# register itself with the {Dispatcher} to listen to a signal that
|
29
29
|
# observable objects may issue.
|
30
|
-
#
|
30
|
+
#
|
31
|
+
# An observer may be anything that is callable, but will usually
|
32
|
+
# be a method, block or Proc object. You may optionally specify an
|
33
|
+
# explicit priority for an observer, to make sure it gets called before
|
34
|
+
# or after other observers.
|
35
|
+
#
|
31
36
|
# @example Using {#connect} to register a new observer
|
32
37
|
# class Logger
|
33
38
|
# def log(event)
|
@@ -36,10 +41,10 @@ module Observatory
|
|
36
41
|
# end
|
37
42
|
# logger = Logger.new
|
38
43
|
# dispatcher.connect('post.publish', logger.method(:log))
|
39
|
-
#
|
44
|
+
#
|
40
45
|
# @example Using {#disconnect} to unregister an observer
|
41
46
|
# dispatcher.disconnect('post.publish', logger.method(:log))
|
42
|
-
#
|
47
|
+
#
|
43
48
|
# @example Using {#notify} to let other objects know something has happened
|
44
49
|
# class Post
|
45
50
|
# include Observable
|
@@ -49,14 +54,14 @@ module Observatory
|
|
49
54
|
# # do publication stuff here
|
50
55
|
# end
|
51
56
|
# end
|
52
|
-
#
|
57
|
+
#
|
53
58
|
# @example Using {#notify_until} to delegate saving a record to another object
|
54
59
|
# class Post
|
55
60
|
# def save
|
56
61
|
# notify_until 'post.save', :title => title
|
57
62
|
# end
|
58
63
|
# end
|
59
|
-
#
|
64
|
+
#
|
60
65
|
# @example Using {#filter} to let observers modify the output of the title attribute
|
61
66
|
# class Post
|
62
67
|
# def title
|
@@ -71,17 +76,23 @@ module Observatory
|
|
71
76
|
def initialize
|
72
77
|
@observers = {}
|
73
78
|
end
|
74
|
-
|
79
|
+
|
75
80
|
# Register a observer for a given signal.
|
76
|
-
#
|
81
|
+
#
|
77
82
|
# Instead of adding a method or Proc object to the stack, you could
|
78
83
|
# also use a block. Either the observer argument or the block is required.
|
79
|
-
#
|
84
|
+
#
|
85
|
+
# Optionally, you could pass in an options hash as the last argument, that
|
86
|
+
# can specify an explicit priority. When omitted, an internal counter starting
|
87
|
+
# from 1 will be used. To make sure your observer is called last, specify
|
88
|
+
# a high, **positive** number. To make sure your observer is called first, specify
|
89
|
+
# a high, **negative** number.
|
90
|
+
#
|
80
91
|
# @example Using a block as an observer
|
81
92
|
# dispatcher.connect('post.publish') do |event|
|
82
93
|
# puts "Post was published"
|
83
94
|
# end
|
84
|
-
#
|
95
|
+
#
|
85
96
|
# @example Using a method as an observer
|
86
97
|
# class Reporter
|
87
98
|
# def log(event)
|
@@ -89,42 +100,98 @@ module Observatory
|
|
89
100
|
# end
|
90
101
|
# end
|
91
102
|
# dispatcher.connect('post.publish', Reporter.new.method(:log))
|
92
|
-
#
|
93
|
-
# @
|
94
|
-
#
|
95
|
-
#
|
96
|
-
#
|
103
|
+
#
|
104
|
+
# @example Determining observer call order using priority
|
105
|
+
# dispatcher.connect('pulp', :priority => 10) do
|
106
|
+
# puts "I dare you!"
|
107
|
+
# end
|
108
|
+
# dispatcher.connect('pulp', :priority => -10) do
|
109
|
+
# puts "I double-dare you!"
|
110
|
+
# end
|
111
|
+
# # output when "pulp" is triggered:
|
112
|
+
# "I double-dare you!"
|
113
|
+
# "I dare you!"
|
114
|
+
#
|
115
|
+
# @overload connect(signal, observer, options = {})
|
116
|
+
# @param [String] signal is the name used by the observable to trigger
|
117
|
+
# observers
|
118
|
+
# @param [#call] observer is the Proc or method that will react to
|
119
|
+
# an event issued by an observable.
|
120
|
+
# @param [Hash] options is an optional Hash of additional options.
|
121
|
+
# @option options [Fixnum] :priority is the priority of this observer
|
122
|
+
# in the stack of all observers for this signal. A higher number means
|
123
|
+
# lower priority. Negative numbers are allowed.
|
124
|
+
# @overload connect(signal, options = {}, &block)
|
125
|
+
# @param [String] signal is the name used by the observable to trigger
|
126
|
+
# observers
|
127
|
+
# @param [Hash] options is an optional Hash of additional options.
|
128
|
+
# @option options [Fixnum] :priority is the priority of this observer
|
129
|
+
# in the stack of all observers for this signal. A higher number means
|
130
|
+
# lower priority. Negative numbers are allowed.
|
97
131
|
# @return [#call] the added observer
|
98
|
-
def connect(signal,
|
99
|
-
|
100
|
-
|
101
|
-
|
132
|
+
def connect(signal, *args, &block)
|
133
|
+
# ugly argument parsing.
|
134
|
+
# Make sure that there is either a block given, or that the second argument is
|
135
|
+
# something callable. If there is a block given, the second argument, if given,
|
136
|
+
# must be a Hash which defaults to an empty Hash. If there is no block given,
|
137
|
+
# the third optional argument must be Hash.
|
138
|
+
if block_given?
|
139
|
+
observer = block
|
140
|
+
if args.size == 1 && args.first.is_a?(Hash)
|
141
|
+
options = args.first
|
142
|
+
elsif args.size == 0
|
143
|
+
options = {}
|
102
144
|
else
|
103
|
-
raise ArgumentError, '
|
145
|
+
raise ArgumentError, 'When given a block, #connect only expects a signal and options hash as arguments'
|
146
|
+
end
|
147
|
+
else
|
148
|
+
observer = args.shift
|
149
|
+
raise ArgumentError, 'Use a block, method or proc to specify an observer' unless observer.respond_to?(:call)
|
150
|
+
if args.any?
|
151
|
+
options = args.shift
|
152
|
+
raise ArgumentError, '#connect only expects a signal, method and options hash as arguments' unless options.is_a?(Hash) || args.any?
|
153
|
+
else
|
154
|
+
options = {}
|
104
155
|
end
|
105
156
|
end
|
157
|
+
|
158
|
+
observer_with_priority = {
|
159
|
+
:observer => observer,
|
160
|
+
:priority => (options[:priority] || next_internal_priority)
|
161
|
+
}
|
162
|
+
|
163
|
+
# Initialize the list of observers for this signal and add this observer
|
106
164
|
observers[signal] ||= []
|
107
|
-
observers[signal] <<
|
165
|
+
observers[signal] << observer_with_priority
|
166
|
+
|
167
|
+
# Sort all observers on priority
|
168
|
+
observers[signal].sort! do |a,b|
|
169
|
+
a[:priority] <=> b[:priority]
|
170
|
+
end
|
171
|
+
|
172
|
+
observer
|
108
173
|
end
|
109
174
|
|
110
175
|
# Removes an observer from a signal stack, so it no longer gets triggered.
|
111
|
-
#
|
176
|
+
#
|
112
177
|
# @param [String] signal is the name of the stack to remove the observer
|
113
178
|
# from.
|
114
179
|
# @param [#call] observer is the original observer to remove.
|
115
180
|
# @return [#call, nil] the removed observer or nil if it could not be found
|
116
181
|
def disconnect(signal, observer)
|
117
182
|
return nil unless observers.key?(signal)
|
118
|
-
observers[signal].
|
183
|
+
observers[signal].delete_if do |observer_with_priority|
|
184
|
+
observer_with_priority[:observer] == observer
|
185
|
+
end
|
119
186
|
end
|
120
187
|
|
121
188
|
# Send out a signal to all registered observers using a new {Event}
|
122
189
|
# instance. The {Event#signal} will be used to determine the stack of
|
123
190
|
# {#observers} to use.
|
124
|
-
#
|
191
|
+
#
|
125
192
|
# Using {#notify} allows observers to take action at a given time during
|
126
193
|
# program execution, such as logging important events.
|
127
|
-
#
|
194
|
+
#
|
128
195
|
# @param [Event]
|
129
196
|
# @return [Event]
|
130
197
|
def notify(event)
|
@@ -136,9 +203,9 @@ module Observatory
|
|
136
203
|
|
137
204
|
# Same as {#notify}, but halt execution as soon as an observer has
|
138
205
|
# indicated it has handled the event by returning a non-falsy value.
|
139
|
-
#
|
206
|
+
#
|
140
207
|
# An event that was acted upon by an observer will be marked as processed.
|
141
|
-
#
|
208
|
+
#
|
142
209
|
# @param [Event]
|
143
210
|
# @see Event#process!
|
144
211
|
# @return [Event]
|
@@ -151,10 +218,10 @@ module Observatory
|
|
151
218
|
|
152
219
|
# Let all registered observers modify a given value. The observable can
|
153
220
|
# then use the {Event#return_value} to get the filtered result back.
|
154
|
-
#
|
221
|
+
#
|
155
222
|
# You could use {#filter} to let observers modify arguments to a method
|
156
223
|
# before continuing to work on them (just an example).
|
157
|
-
#
|
224
|
+
#
|
158
225
|
# @param [Event]
|
159
226
|
# @param [Object] value
|
160
227
|
# @return [Event]
|
@@ -168,8 +235,15 @@ module Observatory
|
|
168
235
|
|
169
236
|
private
|
170
237
|
|
238
|
+
def next_internal_priority
|
239
|
+
@next_internal_priority ||= 0
|
240
|
+
@next_internal_priority += 1
|
241
|
+
end
|
242
|
+
|
171
243
|
def each(signal, &block)
|
172
|
-
(observers[signal] || []).each
|
244
|
+
(observers[signal] || []).each do |observer_with_priority|
|
245
|
+
yield observer_with_priority[:observer]
|
246
|
+
end
|
173
247
|
end
|
174
248
|
end
|
175
249
|
end
|
data/lib/observatory/version.rb
CHANGED
data/observatory.gemspec
CHANGED
@@ -8,7 +8,7 @@ Gem::Specification.new do |s|
|
|
8
8
|
s.platform = Gem::Platform::RUBY
|
9
9
|
s.authors = ['Arjan van der Gaag']
|
10
10
|
s.email = ['arjan@arjanvandergaag.nl']
|
11
|
-
s.homepage =
|
11
|
+
s.homepage = 'http://avdgaag.github.com/observatory'
|
12
12
|
s.summary = "A simple implementation of the observer pattern for Ruby programs."
|
13
13
|
s.description = %q{Observatory is a simple gem to facilitate loosely-coupled communication between Ruby objects. It implements the observer design pattern so that your objects can publish events that other objects can subscribe to. Observatory provides some syntactic sugar and methods to notify events, filter values and allow observing objects to stop the filter chain. Observatory is inspired by the Event Dispatcher Symfony component.}
|
14
14
|
|
@@ -18,8 +18,8 @@ Gem::Specification.new do |s|
|
|
18
18
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
19
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
20
|
s.require_paths = ["lib"]
|
21
|
-
|
22
|
-
s.add_development_dependency 'yard', '~>0.
|
21
|
+
|
22
|
+
s.add_development_dependency 'yard', '~>0.7'
|
23
23
|
s.add_development_dependency 'bluecloth', '~>2.1'
|
24
|
-
s.add_development_dependency 'rake', '~>0.
|
24
|
+
s.add_development_dependency 'rake', '~>0.9'
|
25
25
|
end
|
data/test/dispatcher_test.rb
CHANGED
@@ -5,42 +5,73 @@ class DispatcherTest < Test::Unit::TestCase
|
|
5
5
|
@dispatcher = Dispatcher.new
|
6
6
|
@method = method(:example_observer_method)
|
7
7
|
end
|
8
|
-
|
8
|
+
|
9
9
|
def example_observer_method
|
10
10
|
# does nothing
|
11
11
|
end
|
12
|
-
|
12
|
+
|
13
13
|
def test_should_start_with_empty_list_of_observers
|
14
14
|
assert_equal({}, @dispatcher.observers)
|
15
15
|
end
|
16
|
-
|
16
|
+
|
17
17
|
def test_connecting_an_observer_using_a_method
|
18
18
|
@dispatcher.connect('signal', @method)
|
19
|
-
assert @dispatcher.observers['signal'].include?(@method)
|
19
|
+
assert @dispatcher.observers['signal'].map { |o| o[:observer] }.include?(@method)
|
20
20
|
end
|
21
|
-
|
22
|
-
def
|
21
|
+
|
22
|
+
def test_connecting_an_observer_using_a_block
|
23
23
|
@dispatcher.connect('signal') do
|
24
24
|
# does nothing
|
25
25
|
end
|
26
26
|
assert_equal 1, @dispatcher.observers['signal'].size
|
27
27
|
end
|
28
|
-
|
28
|
+
|
29
|
+
def test_connecting_an_observer_with_a_priority
|
30
|
+
assert_nothing_raised { @dispatcher.connect('signal', @method, :priority => 5) }
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_observers_are_called_in_order
|
34
|
+
guinea_pig = 'foo'
|
35
|
+
@dispatcher.connect('signal', :priority => 10) do
|
36
|
+
guinea_pig = guinea_pig.upcase!
|
37
|
+
end
|
38
|
+
@dispatcher.connect('signal', :priority => 5) do
|
39
|
+
guinea_pig << 'bar'
|
40
|
+
end
|
41
|
+
@dispatcher.notify(Event.new('observable', 'signal'))
|
42
|
+
assert_equal 'FOOBAR', guinea_pig
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_observers_without_priority_remain_in_order_of_adding
|
46
|
+
guinea_pig = 'foo'
|
47
|
+
@dispatcher.connect('signal') do
|
48
|
+
guinea_pig << 'bar'
|
49
|
+
end
|
50
|
+
@dispatcher.connect('signal') do
|
51
|
+
guinea_pig << 'baz'
|
52
|
+
end
|
53
|
+
@dispatcher.connect('signal', :priority => -1) do
|
54
|
+
guinea_pig << 'qux'
|
55
|
+
end
|
56
|
+
@dispatcher.notify(Event.new('observable', 'signal'))
|
57
|
+
assert_equal 'fooquxbarbaz', guinea_pig
|
58
|
+
end
|
59
|
+
|
29
60
|
def test_connecting_nothing_should_raise_exception
|
30
61
|
assert_raise(ArgumentError) { @dispatcher.connect('signal') }
|
31
62
|
end
|
32
|
-
|
63
|
+
|
33
64
|
def test_disconnecting_a_new_observer_returns_nil
|
34
65
|
assert_nil @dispatcher.disconnect('signal', @method)
|
35
66
|
end
|
36
|
-
|
67
|
+
|
37
68
|
def test_disconnecting_an_exisiting_observer_removes_it_from_stack
|
38
69
|
@dispatcher.connect('signal', @method)
|
39
70
|
assert_equal 1, @dispatcher.observers['signal'].size
|
40
71
|
@dispatcher.disconnect('signal', @method)
|
41
72
|
assert_equal 0, @dispatcher.observers['signal'].size
|
42
73
|
end
|
43
|
-
|
74
|
+
|
44
75
|
def test_notify_calls_all_observers
|
45
76
|
flag1 = false
|
46
77
|
flag2 = false
|
@@ -50,7 +81,7 @@ class DispatcherTest < Test::Unit::TestCase
|
|
50
81
|
assert flag1
|
51
82
|
assert flag2
|
52
83
|
end
|
53
|
-
|
84
|
+
|
54
85
|
def test_notify_calls_all_observers_in_order
|
55
86
|
output = ''
|
56
87
|
@dispatcher.connect('signal') { output << 'a' }
|
@@ -58,7 +89,7 @@ class DispatcherTest < Test::Unit::TestCase
|
|
58
89
|
@dispatcher.notify(Event.new('observable', 'signal'))
|
59
90
|
assert_equal('ab', output)
|
60
91
|
end
|
61
|
-
|
92
|
+
|
62
93
|
def test_using_notify_until_calls_all_observers_until_one_returns_true
|
63
94
|
output = ''
|
64
95
|
@dispatcher.connect('signal') { output << 'a'; true }
|
@@ -66,11 +97,11 @@ class DispatcherTest < Test::Unit::TestCase
|
|
66
97
|
@dispatcher.notify_until(Event.new('observable', 'signal'))
|
67
98
|
assert_equal('a', output)
|
68
99
|
end
|
69
|
-
|
100
|
+
|
70
101
|
def test_using_filter_uses_original_value_as_default_return_value
|
71
102
|
assert_equal 'foo', @dispatcher.filter(Event.new('observable', 'signal'), 'foo').return_value
|
72
103
|
end
|
73
|
-
|
104
|
+
|
74
105
|
def test_using_filter_uses_adjusted_value_as_default_return_value
|
75
106
|
@dispatcher.connect('signal') { |e,v| v.upcase }
|
76
107
|
assert_equal 'FOO', @dispatcher.filter(Event.new('observable', 'signal'), 'foo').return_value
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: observatory
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 27
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
- 0
|
9
8
|
- 1
|
10
|
-
|
9
|
+
- 0
|
10
|
+
version: 0.1.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Arjan van der Gaag
|
@@ -15,27 +15,26 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-05-
|
18
|
+
date: 2011-05-26 00:00:00 Z
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
21
|
-
|
22
|
-
|
23
|
-
requirement: &id001 !ruby/object:Gem::Requirement
|
21
|
+
type: :development
|
22
|
+
version_requirements: &id001 !ruby/object:Gem::Requirement
|
24
23
|
none: false
|
25
24
|
requirements:
|
26
25
|
- - ~>
|
27
26
|
- !ruby/object:Gem::Version
|
28
|
-
hash:
|
27
|
+
hash: 5
|
29
28
|
segments:
|
30
29
|
- 0
|
31
|
-
-
|
32
|
-
version: "0.
|
33
|
-
|
34
|
-
version_requirements: *id001
|
35
|
-
- !ruby/object:Gem::Dependency
|
36
|
-
name: bluecloth
|
30
|
+
- 7
|
31
|
+
version: "0.7"
|
32
|
+
requirement: *id001
|
37
33
|
prerelease: false
|
38
|
-
|
34
|
+
name: yard
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
type: :development
|
37
|
+
version_requirements: &id002 !ruby/object:Gem::Requirement
|
39
38
|
none: false
|
40
39
|
requirements:
|
41
40
|
- - ~>
|
@@ -45,23 +44,24 @@ dependencies:
|
|
45
44
|
- 2
|
46
45
|
- 1
|
47
46
|
version: "2.1"
|
48
|
-
|
49
|
-
version_requirements: *id002
|
50
|
-
- !ruby/object:Gem::Dependency
|
51
|
-
name: rake
|
47
|
+
requirement: *id002
|
52
48
|
prerelease: false
|
53
|
-
|
49
|
+
name: bluecloth
|
50
|
+
- !ruby/object:Gem::Dependency
|
51
|
+
type: :development
|
52
|
+
version_requirements: &id003 !ruby/object:Gem::Requirement
|
54
53
|
none: false
|
55
54
|
requirements:
|
56
55
|
- - ~>
|
57
56
|
- !ruby/object:Gem::Version
|
58
|
-
hash:
|
57
|
+
hash: 25
|
59
58
|
segments:
|
60
59
|
- 0
|
61
|
-
-
|
62
|
-
version: "0.
|
63
|
-
|
64
|
-
|
60
|
+
- 9
|
61
|
+
version: "0.9"
|
62
|
+
requirement: *id003
|
63
|
+
prerelease: false
|
64
|
+
name: rake
|
65
65
|
description: Observatory is a simple gem to facilitate loosely-coupled communication between Ruby objects. It implements the observer design pattern so that your objects can publish events that other objects can subscribe to. Observatory provides some syntactic sugar and methods to notify events, filter values and allow observing objects to stop the filter chain. Observatory is inspired by the Event Dispatcher Symfony component.
|
66
66
|
email:
|
67
67
|
- arjan@arjanvandergaag.nl
|
@@ -93,7 +93,7 @@ files:
|
|
93
93
|
- test/observer_test.rb
|
94
94
|
- test/test_helper.rb
|
95
95
|
- watch_tests.rb
|
96
|
-
homepage:
|
96
|
+
homepage: http://avdgaag.github.com/observatory
|
97
97
|
licenses: []
|
98
98
|
|
99
99
|
post_install_message:
|