eventful 1.0.0
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/History.txt +4 -0
- data/Manifest.txt +6 -0
- data/README.txt +124 -0
- data/Rakefile +12 -0
- data/lib/eventful.rb +108 -0
- data/test/test_eventful.rb +85 -0
- metadata +83 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
data/README.txt
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
= Eventful
|
|
2
|
+
|
|
3
|
+
* http://github.com/jcoglan/eventful
|
|
4
|
+
|
|
5
|
+
+Eventful+ is a small extension on top of Ruby's +Observable+ module that
|
|
6
|
+
implements named events, block listeners and event bubbling. It allows
|
|
7
|
+
much more flexible event handling behaviour than is typically allowed
|
|
8
|
+
by +Observable+, which requires listeners to be objects that implement
|
|
9
|
+
+update+ and provides no simple way of calling subsets of observers based
|
|
10
|
+
on event type.
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
== Installation
|
|
14
|
+
|
|
15
|
+
sudo gem install eventful
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
== Examples
|
|
19
|
+
|
|
20
|
+
Make a class listenable by mixing +Eventful+ into it:
|
|
21
|
+
|
|
22
|
+
class Watcher
|
|
23
|
+
include Eventful
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
Register event listeners using +on+ with an event name and a block.
|
|
27
|
+
Publish events using +fire+ with the event name. The block accepts
|
|
28
|
+
the object that published the event, along with any parameters passed
|
|
29
|
+
to +fire+.
|
|
30
|
+
|
|
31
|
+
w = Watcher.new
|
|
32
|
+
|
|
33
|
+
w.on(:filechange) { |watcher, path| puts path }
|
|
34
|
+
w.on(:filedelete) { |watcher, path| puts "#{ watcher } deleted #{ path }" }
|
|
35
|
+
|
|
36
|
+
w.fire(:filechange, '/path/to/file.txt')
|
|
37
|
+
w.fire(:filedelete, '/tmp/pids/event.pid')
|
|
38
|
+
|
|
39
|
+
# prints...
|
|
40
|
+
# /path/to/file.txt
|
|
41
|
+
# #<Watcher:0xb7b485a4> deleted /tmp/pids/event.pid
|
|
42
|
+
|
|
43
|
+
The +on+ method returns the +Observer+ object used to represent the listener,
|
|
44
|
+
so you can remove it using +delete_observer+.
|
|
45
|
+
|
|
46
|
+
obs = w.on(:filechange) { |watcher| ... }
|
|
47
|
+
|
|
48
|
+
# listener will not fire after this
|
|
49
|
+
w.delete_observer(obs)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
=== Method chains instead of blocks
|
|
53
|
+
|
|
54
|
+
Instead of passing a block, you can add behaviour to objects by chaining
|
|
55
|
+
method calls after the +on+ call. For example:
|
|
56
|
+
|
|
57
|
+
class Logger
|
|
58
|
+
include Eventful
|
|
59
|
+
|
|
60
|
+
def print(message)
|
|
61
|
+
puts message
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
log = Logger.new
|
|
66
|
+
log.on(:receive).print "Received message"
|
|
67
|
+
|
|
68
|
+
# Calls `log.print "Received message"`
|
|
69
|
+
log.fire(:receive)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
=== Events that bubble
|
|
73
|
+
|
|
74
|
+
When you +fire+ an event, the event 'bubbles' up the type system. What
|
|
75
|
+
this means is that you can listen to events on all the instances of a
|
|
76
|
+
class just by placing an event listener on the class itself. As above,
|
|
77
|
+
the listener is called with the instance that fired the event.
|
|
78
|
+
|
|
79
|
+
Logger.on(:receive) { |log, msg| puts "#{ log } :: #{ msg }" }
|
|
80
|
+
|
|
81
|
+
l1, l2 = Logger.new, Logger.new
|
|
82
|
+
|
|
83
|
+
l1.fire(:receive, 'The first message')
|
|
84
|
+
l2.fire(:receive, 'Another event')
|
|
85
|
+
|
|
86
|
+
# prints...
|
|
87
|
+
# #<Logger:0xb7bf103c> :: The first message
|
|
88
|
+
# #<Logger:0xb7bf1028> :: Another event
|
|
89
|
+
|
|
90
|
+
Method chains can also be used, and they will be replayed on the instance
|
|
91
|
+
that initiated the event.
|
|
92
|
+
|
|
93
|
+
# Calls `log.print "Received message"`
|
|
94
|
+
|
|
95
|
+
Logger.on(:receive).print "Received message"
|
|
96
|
+
|
|
97
|
+
log = Logger.new
|
|
98
|
+
log.fire(:receive)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
== License
|
|
102
|
+
|
|
103
|
+
(The MIT License)
|
|
104
|
+
|
|
105
|
+
Copyright (c) 2009 James Coglan
|
|
106
|
+
|
|
107
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
108
|
+
a copy of this software and associated documentation files (the
|
|
109
|
+
'Software'), to deal in the Software without restriction, including
|
|
110
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
111
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
112
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
113
|
+
the following conditions:
|
|
114
|
+
|
|
115
|
+
The above copyright notice and this permission notice shall be
|
|
116
|
+
included in all copies or substantial portions of the Software.
|
|
117
|
+
|
|
118
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
|
119
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
120
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
121
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
122
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
123
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
124
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# -*- ruby -*-
|
|
2
|
+
|
|
3
|
+
require 'rubygems'
|
|
4
|
+
require 'hoe'
|
|
5
|
+
|
|
6
|
+
Hoe.spec 'eventful' do |p|
|
|
7
|
+
# self.rubyforge_name = 'eventfulx' # if different than 'eventful'
|
|
8
|
+
p.developer('James Coglan', 'jcoglan@googlemail.com')
|
|
9
|
+
p.extra_deps = %w[methodphitamine]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# vim: syntax=ruby
|
data/lib/eventful.rb
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
require 'observer'
|
|
2
|
+
require 'rubygems'
|
|
3
|
+
require 'methodphitamine'
|
|
4
|
+
|
|
5
|
+
# Adds named event publishing capabilities to the class that includes it.
|
|
6
|
+
# Actually composed of three modules: +Observable+ (from Ruby stdlib),
|
|
7
|
+
# +ObservableWithBlocks+, and +Eventful+ itself. See +README+ for examples.
|
|
8
|
+
module Eventful
|
|
9
|
+
VERSION = '1.0.0'
|
|
10
|
+
|
|
11
|
+
# The +Observer+ class is used to wrap blocks in an object that implements
|
|
12
|
+
# +update+, so it can be used with +Observable+. It extends <tt>Methodphitamine::It</tt>,
|
|
13
|
+
# meaning it can store any methods called on it and replay them later on any
|
|
14
|
+
# object. This is used to implement blockless event handlers.
|
|
15
|
+
class Observer < Methodphitamine::It
|
|
16
|
+
# Initalize using a block. The block will be called when the observed
|
|
17
|
+
# object sends notifications.
|
|
18
|
+
def initialize(&block)
|
|
19
|
+
super()
|
|
20
|
+
@block = block
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Called by the observed object inside +Observable+ to publish events.
|
|
24
|
+
def update(*args)
|
|
25
|
+
@block.call(*args)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Patch these back in after Methodphitamine removes them. They are
|
|
29
|
+
# needed for +Observable+ to handle the object properly.
|
|
30
|
+
%w[respond_to? hash send].each do |sym|
|
|
31
|
+
define_method(sym) do |*args|
|
|
32
|
+
Object.instance_method(sym).bind(self).call(*args)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Extends the +Observable+ module and allows it to accept blocks as observers.
|
|
38
|
+
# This is a distinct module because I often want to do this without using
|
|
39
|
+
# named events.
|
|
40
|
+
module ObservableWithBlocks
|
|
41
|
+
include Observable
|
|
42
|
+
|
|
43
|
+
# Adds an observer to the object. The observer may be an object implementing
|
|
44
|
+
# +update+, or a block that will be called when the object notifies observers.
|
|
45
|
+
# If a block is passed, we return the wrapping +Observer+ object so it can
|
|
46
|
+
# removed using +delete_observer+.
|
|
47
|
+
def add_observer(*args, &block)
|
|
48
|
+
return super unless block_given?
|
|
49
|
+
observer = Observer.new(&block)
|
|
50
|
+
add_observer(observer)
|
|
51
|
+
observer
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Mix in block observer support
|
|
56
|
+
include ObservableWithBlocks
|
|
57
|
+
|
|
58
|
+
# Registers a named event handler on the target object that will only fire
|
|
59
|
+
# when the object publishes events with the given name. The handler should
|
|
60
|
+
# be a block that will accept the object that fired the event, along with
|
|
61
|
+
# and data published with the event. Returns a <tt>Methodphitamine::It</tt>
|
|
62
|
+
# instance that will be replayed on the publishing object when the event fires.
|
|
63
|
+
#
|
|
64
|
+
# See +README+ for examples.
|
|
65
|
+
def on(event, &block)
|
|
66
|
+
observer = add_observer do |*args|
|
|
67
|
+
type, data = args[1], [args[0]] + args[2..-1]
|
|
68
|
+
if type == event
|
|
69
|
+
block ||= observer.to_proc
|
|
70
|
+
block.call(*data)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Fires a named event on the target object. The first argument should be a
|
|
76
|
+
# symbol representing the event name. Any subsequent arguments are passed
|
|
77
|
+
# listeners along with the publishing object. The event bubbles up the type
|
|
78
|
+
# system so that you can listen to all objects of a given type by regsitering
|
|
79
|
+
# listeners on their class.
|
|
80
|
+
def fire(*args)
|
|
81
|
+
return self if defined?(@observer_state) and not @observer_state
|
|
82
|
+
|
|
83
|
+
receiver = (Hash === args.first) ? args.shift[:receiver] : self
|
|
84
|
+
args = [receiver] + args
|
|
85
|
+
|
|
86
|
+
changed(true)
|
|
87
|
+
notify_observers(*args)
|
|
88
|
+
changed(true)
|
|
89
|
+
|
|
90
|
+
args[0] = {:receiver => receiver}
|
|
91
|
+
self.class.ancestors.grep(Eventful).each &it.fire(*args)
|
|
92
|
+
|
|
93
|
+
self
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Classes that +include+ +Eventful+ are also extended with it, so that event
|
|
97
|
+
# listeners can be registered on a class to fire whenever an instance of
|
|
98
|
+
# that class publishes an event.
|
|
99
|
+
def self.included(base)
|
|
100
|
+
base.extend(self)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Extend +Eventful+ with itself so you can listen to all eventful objects
|
|
104
|
+
# using <tt>Eventful.on(:eventname) { handle_event() }</tt>.
|
|
105
|
+
extend(self)
|
|
106
|
+
|
|
107
|
+
end
|
|
108
|
+
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
require "test/unit"
|
|
2
|
+
require "eventful"
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
class Foo
|
|
6
|
+
include Eventful
|
|
7
|
+
attr_reader :count
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@count = 0
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def bump!(x = 1)
|
|
14
|
+
@count += x
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class Bar
|
|
19
|
+
include Eventful
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class TestEventful < Test::Unit::TestCase
|
|
23
|
+
def setup
|
|
24
|
+
[Foo, Bar, Eventful].each &it.delete_observers
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def test_named_events
|
|
28
|
+
ayes, noes = 0, 0
|
|
29
|
+
f = Foo.new
|
|
30
|
+
f.on(:aye) { |foo, x| ayes += x }
|
|
31
|
+
obs = f.on(:noe) { |foo, x| noes += x }
|
|
32
|
+
|
|
33
|
+
f.fire(:aye, 1)
|
|
34
|
+
assert_equal 1, ayes
|
|
35
|
+
assert_equal 0, noes
|
|
36
|
+
f.fire(:noe, 3)
|
|
37
|
+
assert_equal 1, ayes
|
|
38
|
+
assert_equal 3, noes
|
|
39
|
+
|
|
40
|
+
f.delete_observer(obs)
|
|
41
|
+
f.fire(:noe, 3)
|
|
42
|
+
assert_equal 1, ayes
|
|
43
|
+
assert_equal 3, noes
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def test_chaining
|
|
47
|
+
f = Foo.new
|
|
48
|
+
f.on(:aye).bump! 2
|
|
49
|
+
f.on(:noe).bump! -1
|
|
50
|
+
|
|
51
|
+
2.times { f.fire(:aye) }
|
|
52
|
+
f.fire(:noe)
|
|
53
|
+
|
|
54
|
+
assert_equal 3, f.count
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def test_bubbling
|
|
58
|
+
bar1, bar2 = Bar.new, Bar.new
|
|
59
|
+
list = []
|
|
60
|
+
Bar.on(:aye) { |r| list << r }
|
|
61
|
+
Eventful.on(:aye) { |r| list << r }
|
|
62
|
+
Eventful.on(:noe) { |r| list << r }
|
|
63
|
+
|
|
64
|
+
bar1.fire(:aye)
|
|
65
|
+
assert_equal [bar1, bar1], list
|
|
66
|
+
bar2.fire(:noe)
|
|
67
|
+
assert_equal [bar1, bar1, bar2], list
|
|
68
|
+
|
|
69
|
+
Bar.fire(:aye)
|
|
70
|
+
assert_equal [bar1, bar1, bar2, Bar], list
|
|
71
|
+
Bar.fire(:noe)
|
|
72
|
+
assert_equal [bar1, bar1, bar2, Bar], list
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def test_chaining_on_bubble
|
|
76
|
+
f1, f2 = Foo.new, Foo.new
|
|
77
|
+
Foo.on(:aye).bump! 5
|
|
78
|
+
f1.fire(:aye)
|
|
79
|
+
assert_equal 5, f1.count
|
|
80
|
+
assert_equal 0, f2.count
|
|
81
|
+
f2.fire(:aye)
|
|
82
|
+
assert_equal 5, f1.count
|
|
83
|
+
assert_equal 5, f2.count
|
|
84
|
+
end
|
|
85
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: eventful
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- James Coglan
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
|
|
12
|
+
date: 2009-06-18 00:00:00 +01:00
|
|
13
|
+
default_executable:
|
|
14
|
+
dependencies:
|
|
15
|
+
- !ruby/object:Gem::Dependency
|
|
16
|
+
name: methodphitamine
|
|
17
|
+
type: :runtime
|
|
18
|
+
version_requirement:
|
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
20
|
+
requirements:
|
|
21
|
+
- - ">="
|
|
22
|
+
- !ruby/object:Gem::Version
|
|
23
|
+
version: "0"
|
|
24
|
+
version:
|
|
25
|
+
- !ruby/object:Gem::Dependency
|
|
26
|
+
name: hoe
|
|
27
|
+
type: :development
|
|
28
|
+
version_requirement:
|
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: 2.0.0
|
|
34
|
+
version:
|
|
35
|
+
description: ""
|
|
36
|
+
email:
|
|
37
|
+
- jcoglan@googlemail.com
|
|
38
|
+
executables: []
|
|
39
|
+
|
|
40
|
+
extensions: []
|
|
41
|
+
|
|
42
|
+
extra_rdoc_files:
|
|
43
|
+
- History.txt
|
|
44
|
+
- Manifest.txt
|
|
45
|
+
- README.txt
|
|
46
|
+
files:
|
|
47
|
+
- History.txt
|
|
48
|
+
- Manifest.txt
|
|
49
|
+
- README.txt
|
|
50
|
+
- Rakefile
|
|
51
|
+
- lib/eventful.rb
|
|
52
|
+
- test/test_eventful.rb
|
|
53
|
+
has_rdoc: true
|
|
54
|
+
homepage: http://github.com/jcoglan/eventful
|
|
55
|
+
licenses: []
|
|
56
|
+
|
|
57
|
+
post_install_message:
|
|
58
|
+
rdoc_options:
|
|
59
|
+
- --main
|
|
60
|
+
- README.txt
|
|
61
|
+
require_paths:
|
|
62
|
+
- lib
|
|
63
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: "0"
|
|
68
|
+
version:
|
|
69
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - ">="
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: "0"
|
|
74
|
+
version:
|
|
75
|
+
requirements: []
|
|
76
|
+
|
|
77
|
+
rubyforge_project: eventful
|
|
78
|
+
rubygems_version: 1.3.3
|
|
79
|
+
signing_key:
|
|
80
|
+
specification_version: 3
|
|
81
|
+
summary: ""
|
|
82
|
+
test_files:
|
|
83
|
+
- test/test_eventful.rb
|