eventful 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|