gbs-signal-slot 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/README ADDED
@@ -0,0 +1,215 @@
1
+ = GBS::SignalSlot v. 1.0.0
2
+ Greenblack Software signal/slot pattern implementation for Ruby language
3
+
4
+ == GBS::SignalSlot User's Guide
5
+
6
+ 1. Introduction
7
+ 2. Connecting slots
8
+ 3. Disconnecting slots
9
+ 4. Class signals
10
+ 5. MIT License
11
+
12
+ === Introduction
13
+
14
+ GBS::SignalSlot is a signal/slot mechanism implementation done in Ruby.
15
+ The signal/slot pattern is a way of implementing the Observer design pattern.
16
+ Such implementation was introduced by Trolltech in their famous QT library.
17
+ Indeed the goal was achieved and resulted in code size reduction,
18
+ especially so called boilerplate code.
19
+
20
+ With the Ruby facilities of meta-programming the whole implementation can be
21
+ done within one single file with just a couple of lines. Such implementation was
22
+ proposed by Niklas Frykholm and improved by Nobuyoshi Nakada on <b>comp.lang.ruby</b>
23
+ usenet group (01-02-2002). Their work was the entry point to the Greenblack Software
24
+ implementation. In fact, the idea and main tricks are the same. Our improvements
25
+ were concerning on class methods support, better error checking, some additional
26
+ features and clean output.
27
+
28
+ === Connecting slots
29
+
30
+ The best way to see what GBS::SignalSlot is and to get into it a bit more, is to look
31
+ thoroughly at the following examples.
32
+
33
+ First, let we create a class, a container for a value. Of course with signals
34
+ our container will emit.
35
+
36
+ require "rubygems"
37
+ require "gbs_signal_slot"
38
+
39
+ class ValueContainer
40
+ include GBS::SignalSlot
41
+ attr_reader :value
42
+ signal :new_value # signals are defined just like readers or writers
43
+
44
+ def initialize(v)
45
+ @value = v
46
+ end
47
+
48
+ def value=(v)
49
+ @value = v
50
+ new_value(v) # signal activation
51
+ end
52
+ end
53
+
54
+ As you can guess, the signals are defined just like obvious attributes and
55
+ emitted by calling a method named after the symbol indicating a signal.
56
+ Their parameters are matter of our arbitrary choice, however we should
57
+ stick to our once defined convention. Here, we are passing the new
58
+ value that our container gets.
59
+
60
+ Particularly, the _new_value(v)_ method does not exist in the code.
61
+ It is added by the module behind the scene, as a private method.
62
+
63
+ Next, we can use some mixed-in methods that will allow us to connect and
64
+ disconnect signals and slots. The simplest way is to connect to anonymous closures:
65
+
66
+ vc = ValueContainer.new(0)
67
+ vc.connect(:new_value) { |v| puts "New value is #{v}" }
68
+
69
+ Now, each use of the value writer will result in sending relevant string
70
+ to the output:
71
+
72
+ vc.value = 1
73
+ # >> New value is 1
74
+
75
+ But to get more fun let's make a _Tracker_ object with both class
76
+ and instance methods which will act as slots for our _new_value_ signal.
77
+
78
+ class Tracker
79
+ def track_value(v)
80
+ puts "value changed to #{v}!"
81
+ end
82
+ def self.track_value(v)
83
+ puts "class slot method executed"
84
+ end
85
+ end
86
+
87
+ Now we may connect our signal to newly created tracker's slots.
88
+
89
+ t = Tracker.new
90
+ vc.connect(:new_value, :track_value, t) # track_value instance method
91
+ vc.connect(:new_value, :track_value, Tracker) # track_value class method
92
+ vc.value = 2
93
+
94
+ # >> New value is 2
95
+ # >> value changed to 2
96
+ # >> class slot method excecuted
97
+
98
+ The last possibility is to connect signal to a global method. Like below:
99
+
100
+ def track_fun(v)
101
+ puts "Global function"
102
+ end
103
+
104
+ vc.connect(:new_value, :track_fun) # that's all
105
+ vc.value = 3
106
+
107
+ # >> New value is 3
108
+ # >> value changed to 3
109
+ # >> class slot method excecuted
110
+ # >> Global function
111
+
112
+ It is so easy because the _obj_ parameter is set to _Object_ by default. Every global
113
+ method we create belongs to the _Object_ class, so we do not have to specify
114
+ anything else at all.
115
+
116
+ === Disconnecting slots
117
+
118
+ Ok, now it is time to clean everything up. The connected slots can be disconnected
119
+ using a somewhat contrary method: _disconnect_. Let's get the rid of the global function:
120
+
121
+ vc.disconnect(:new_value, :track_fun)
122
+ vc.value = 4
123
+
124
+ # >> New value is 4
125
+ # >> value changed to 4
126
+ # >> class slot method excecuted
127
+
128
+ As we see the global function call is removed. Similarly we may cut off other slot methods:
129
+
130
+ vc.disconnect(:new_value, :track_value, t)
131
+ vc.disconnect(:new_value, :track_value, Tracker)
132
+ vc.value = 5
133
+
134
+ # >> New value is 5
135
+
136
+ Ok. But how can we deal with the anonymous closure? There are two ways. The first one
137
+ is to get the name, a reference saying precisely, to the proc object. The second way
138
+ is to cut off all slots at once. Here is the action:
139
+
140
+ blah_proc = vc.connect(:new_value) { |v| puts "blah" } # returns the proc object
141
+ vc.value = 6
142
+
143
+ # >> New value is 6
144
+ # >> blah
145
+
146
+ vc.disconnect(:new_value, &blah_proc) # disconnecting the proc object
147
+ vc.value = 7
148
+
149
+ # >> New value is 7
150
+
151
+ If we do not have the proc object, the only way is to disconnect all slots.
152
+
153
+ vc.disconnect(:new_value) # disconnects all slots connected to the :new_value
154
+ vc.value = 8
155
+
156
+ # Nothing to print.
157
+
158
+ === Class signals
159
+
160
+ GBS::SignalSlot module can be used with class methods as well
161
+ (within classes or modules). The only difference is the use of
162
+ _class_signal_ method instead of _signal_.
163
+
164
+ class SomeClass
165
+ include GBS::SignalSlot
166
+
167
+ signal :signal1
168
+ class_signal :signal2
169
+
170
+ def some_method
171
+ signal1 # activation
172
+ end
173
+
174
+ def self.some_other_method
175
+ signal2 # class signal activation
176
+ end
177
+ end
178
+
179
+ some_instance.connect(:signal1) { puts "instance signal" }
180
+ SomeClass.connect(:signal2) { puts "class signal" }
181
+ # etc.
182
+
183
+ The way we activate signals is still identical. It is even possible
184
+ to use the same signal symbol for both contexts (class and instance)
185
+ at the same time. The contexts will not be messed up and the right
186
+ signals will activate right slots as we had defined before.
187
+
188
+ Such distinction (between _signal_ and _class_signal_) is due to readiness
189
+ of class/module definitions. From the technical point of view it is easy to
190
+ embed class and instance level behaviors into one method. The other reason is
191
+ the fewer amount of code generated and inserted into classes.
192
+
193
+ === MIT License
194
+
195
+ The MIT License
196
+
197
+ Copyright (c) 2008 Greenblack Software
198
+
199
+ Permission is hereby granted, free of charge, to any person obtaining a copy
200
+ of this software and associated documentation files (the "Software"), to deal
201
+ in the Software without restriction, including without limitation the rights
202
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
203
+ copies of the Software, and to permit persons to whom the Software is
204
+ furnished to do so, subject to the following conditions:
205
+
206
+ The above copyright notice and this permission notice shall be included in
207
+ all copies or substantial portions of the Software.
208
+
209
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
210
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
211
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
212
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
213
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
214
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
215
+ THE SOFTWARE.
@@ -0,0 +1,159 @@
1
+ # Copyright (c) 2008 Greenblack Software. Licensed under the MIT License.
2
+ # For more info please read the README or visit www.greenblacksoftware.com
3
+ # ----
4
+ # Based on Niklas Frykholm and Nobuyoshi Nakada signal/slot pattern implementations
5
+ # posted to comp.lang.ruby usenet group on 01-02-2002
6
+
7
+ # A default namespace used by Greenblack Software projects.
8
+ module GBS
9
+
10
+ # A module providing signal/slot mechanism.
11
+ # ----
12
+ # Copyright (c) 2008 Greenblack Software. Licensed under the MIT License.
13
+ # For more info please read the README or visit www.greenblacksoftware.com
14
+ # ----
15
+ # Based on Niklas Frykholm and Nobuyoshi Nakada signal/slot pattern implementations
16
+ # posted to comp.lang.ruby usenet group on 01-02-2002
17
+ module SignalSlot
18
+
19
+ # Connects a signal to a slot. The slot might be either a method referenced as
20
+ # a symbol (often together with a class or an instance) or just an anonymous closure
21
+ # passed as a block.
22
+ #
23
+ # :call-seq:
24
+ # sth.connect(signal) { ... } -> proc
25
+ # sth.connect(signal, slot) -> global slot method
26
+ # sth.connect(signal, slot, obj) -> obj.slot method
27
+ #
28
+ # === Parameters
29
+ # [+signal+]
30
+ # symbol defined in a class with the +signal+ or +class_signal+ method
31
+ # [+slot+]
32
+ # a method name referenced as a symbol object. If ommited the +connect+
33
+ # will try to bind a block
34
+ # [+obj+]
35
+ # an instance (for instance method), a class (for class methods)
36
+ # or nothing for global methods
37
+ #
38
+ # === Returns
39
+ # * determined procedure (method) object or +nil+ if only the +signal+ parameter
40
+ # was passed (without a block or a slot method name). It is important for
41
+ # disconnecting anonymous proc objects because this is the only way to obtain
42
+ # the reference. See also +GBS::SignalSlot#disconnect+
43
+ # ----
44
+ # === Examples
45
+ # [+sth.connect(:value_changed, :update_control, self)+]
46
+ # if the signal +:value_changed+ is emitted, the +self#update_control+
47
+ # will be executed
48
+ # [+sth.connect(:value_changed, update_status, MyClass)+]
49
+ # similar example but with +MyClass.update_status+ class method instead
50
+ # [+sth.connect(:value_changed, :global_update)+]
51
+ # now we are reffering to a global function (to something that
52
+ # belongs to the +Object+ class in fact as the default value of +obj+
53
+ # is set to +Object+)
54
+ # [+sth.connect(:value_changed) { |value| puts value }+]
55
+ # passing anonymous closure
56
+ def connect(signal, slot = nil, obj = Object, &pr)
57
+ s = slot ? obj.method(slot) : pr
58
+ raise ArgumentError, "No slot provided" unless s
59
+ ((@_gbs_signals ||= {})[signal] ||= []) << s
60
+ s
61
+ end
62
+
63
+ # Disconnects a signal from a slot. The slot might be either a method referenced as
64
+ # a symbol together with a class or an instance or just an closure passed as a parameter
65
+ # (with & prefix)
66
+ #
67
+ # :call-seq:
68
+ # sth.disconnect(signal)
69
+ # sth.disconnect(signal, &proc)
70
+ # sth.disconnect(signal, slot)
71
+ # sth.disconnect(signal, slot, obj)
72
+ #
73
+ # === Parameters
74
+ # [+signal+]
75
+ # symbol defined in a class with the +signal+ or +class_signal+ method
76
+ # [+slot+]
77
+ # a method name referenced as a symbol object. If ommited the +connect+
78
+ # will try to bind the +&proc+. If the +&proc+ parameter
79
+ # is also omitted, all connections to the signal will be erased.
80
+ # It disconnects all slots.
81
+ # [+obj+]
82
+ # an instance (for instance method), a class (for class methods)
83
+ # or nothing for global methods
84
+ # [+&proc+]
85
+ # a proc object appended with the & prefix. This object can be
86
+ # obtained by the +GBS::SignalSlot#connect+ method with anonymous closure
87
+ # passed.
88
+ # ----
89
+ # === Examples
90
+ # [+sth.disconnect(:value_changed, :update_control, self)+]
91
+ # if the signal +:value_changed+ is emitted,
92
+ # the +self#update_control+ will not be executed anymore
93
+ # [+sth.disconnect(:value_changed, update_status, MyClass)+]
94
+ # similar example but with +MyClass.update_status+ class method instead
95
+ # [+sth.disconnect(:value_changed, :global_update)+]
96
+ # now we are reffering to a global function (to something that
97
+ # belongs to the +Object+ class in fact as the default value of +obj+
98
+ # is set to +Object+)
99
+ # [+sth.disconnect(:value_changed, &proc)+]
100
+ # the +proc+ object will not be called anymore on
101
+ # +:value_changed+ signal activation.
102
+ # [+sth.disconnect(:value_changed)+]
103
+ # +:value_changed+ totally disconnected from all slots
104
+ def disconnect(signal, slot = nil, obj = Object, &pr)
105
+ s = slot ? obj.method(slot) : pr
106
+ return unless @_gbs_signals and @_gbs_signals[signal]
107
+ if s
108
+ @_gbs_signals[signal].delete(s)
109
+ else
110
+ @_gbs_signals[signal] = []
111
+ end
112
+ end
113
+
114
+ def _gbs_activate_signal(signal, *args, &pr)
115
+ return unless @_gbs_signals and @_gbs_signals[signal]
116
+ # little support for asynchronous connection
117
+ # adding/removing during execution
118
+ slots = @_gbs_signals[signal].clone
119
+ slots.each { |slot| slot.call(*args, &pr) }
120
+ end
121
+
122
+ module Sender
123
+
124
+ # Defines signals allowed to emit by an instance methods.
125
+ # Signals should be provided as symbols and activated by calling instance methods
126
+ # named after corresponding symbols. Those methods are appended dynamically.
127
+ def signal(*signals) # :doc:
128
+ signals.each { |s| module_eval method_str(s, false) }
129
+ end
130
+
131
+ # Defines signals allowed to emit by class methods.
132
+ # Signals should be provided as symbols and activated by calling class methods
133
+ # named after corresponding symbols. Those methods are appended dynamically.
134
+ def class_signal(*signals) # :doc:
135
+ signals.each { |s| module_eval method_str(s, true) }
136
+ end
137
+
138
+ def method_str(signal, klass)
139
+ %Q{
140
+ def #{klass ? "self." : ""}#{signal}(*args, &pr)
141
+ _gbs_activate_signal(:#{signal}, *args, &pr)
142
+ end
143
+ private#{klass ? "_class_method" : ""} :#{signal}
144
+ }
145
+ end
146
+
147
+ private :signal, :class_signal, :method_str
148
+ end
149
+
150
+ def self.append_features(klass)
151
+ super
152
+ klass.extend(Sender)
153
+ klass.extend(SignalSlot)
154
+ end
155
+
156
+ private :_gbs_activate_signal
157
+ private_class_method :append_features
158
+ end
159
+ end
@@ -0,0 +1,175 @@
1
+ # Copyright (c) 2008 Greenblack Software. Licensed under the MIT License.
2
+ # For more info please read the README or visit www.greenblacksoftware.com
3
+
4
+ $:.unshift File.join(File.dirname(__FILE__),'..','lib')
5
+
6
+ require "test/unit"
7
+ require "gbs_signal_slot"
8
+
9
+ $global_mock_value = nil
10
+
11
+ def global_mock_method(text)
12
+ $global_mock_value = text
13
+ end
14
+
15
+ class TestSignalSlot < Test::Unit::TestCase
16
+ @@class_mock_value = nil
17
+
18
+ def setup
19
+ @val1 = nil
20
+ @val2 = nil
21
+ @@class_mock_value = nil
22
+ end
23
+
24
+ class InstanceTest
25
+ include GBS::SignalSlot
26
+
27
+ signal :test_signal
28
+
29
+ def fire_test_sig(t)
30
+ test_signal(t)
31
+ end
32
+ end
33
+
34
+ module ModuleTest
35
+ include GBS::SignalSlot
36
+
37
+ class_signal :class_test_signal
38
+
39
+ def self.fire_class_test_sig(t)
40
+ class_test_signal(t)
41
+ end
42
+ end
43
+
44
+ class InstanceClassTest
45
+ include GBS::SignalSlot
46
+
47
+ signal :test_signal
48
+ class_signal :class_test_signal
49
+
50
+ def fire_test_sig(t)
51
+ test_signal(t)
52
+ end
53
+
54
+ def self.fire_class_test_sig(t)
55
+ class_test_signal(t)
56
+ end
57
+ end
58
+
59
+ def mock_slot_method(text)
60
+ @val1 = text
61
+ end
62
+
63
+ def self.class_mock_method(text)
64
+ @@class_mock_value = text
65
+ end
66
+
67
+ def test_connect_instance
68
+ it = InstanceTest.new
69
+ it.connect(:test_signal) { |text| @val2 = text }
70
+ it.connect(:test_signal, :mock_slot_method, self)
71
+ it.connect(:test_signal, :class_mock_method, TestSignalSlot)
72
+ it.connect(:test_signal, :global_mock_method)
73
+ it.fire_test_sig("a")
74
+ assert_equal("a", @val2)
75
+ assert_equal("a", @val1)
76
+ assert_equal("a", @@class_mock_value)
77
+ assert_equal("a", $global_mock_value)
78
+ end
79
+
80
+ def test_connect_module
81
+ ModuleTest.connect(:class_test_signal) { |text| @val2 = text }
82
+ ModuleTest.connect(:class_test_signal, :mock_slot_method, self)
83
+ ModuleTest.fire_class_test_sig("b")
84
+ assert_equal("b", @val2)
85
+ assert_equal("b", @val1)
86
+ end
87
+
88
+ def test_connect_mixed
89
+ obj = InstanceClassTest.new
90
+ obj.connect(:test_signal) { |text| @val2 = text }
91
+ obj.connect(:test_signal, :mock_slot_method, self)
92
+ obj.fire_test_sig("a")
93
+ assert_equal("a", @val2)
94
+ assert_equal("a", @val1)
95
+ InstanceClassTest.connect(:class_test_signal) { |text| @val2 = text }
96
+ InstanceClassTest.connect(:class_test_signal, :mock_slot_method, self)
97
+ InstanceClassTest.fire_class_test_sig("b")
98
+ assert_equal("b", @val2)
99
+ assert_equal("b", @val1)
100
+ end
101
+
102
+ def test_disconnect
103
+ it = InstanceTest.new
104
+ proc = it.connect(:test_signal) { |text| @val2 = text }
105
+ it.connect(:test_signal, :mock_slot_method, self)
106
+ it.fire_test_sig(1)
107
+ assert_equal(1, @val2)
108
+ assert_equal(1, @val1)
109
+ @val2 = @val1 = nil
110
+ it.disconnect(:test_signal, &proc)
111
+ it.fire_test_sig(1)
112
+ assert_nil(@val2)
113
+ assert_not_nil(@val1)
114
+ @val2 = @val1 = nil
115
+ it.disconnect(:test_signal, :mock_slot_method, self)
116
+ it.fire_test_sig(1)
117
+ assert_nil(@val2)
118
+ assert_nil(@val1)
119
+ end
120
+
121
+ def test_disconnect_all
122
+ it = InstanceTest.new
123
+ it.connect(:test_signal) { |t| @val2 = t }
124
+ it.connect(:test_signal) { |t| @val1 = t }
125
+ it.fire_test_sig(1)
126
+ assert_equal(1, @val2)
127
+ assert_equal(1, @val1)
128
+ it.disconnect(:test_signal)
129
+ it.fire_test_sig(2)
130
+ assert_not_equal(2, @val2)
131
+ assert_not_equal(2, @val1)
132
+ end
133
+
134
+ def test_signal_class_instance
135
+ ic = InstanceClassTest.new
136
+ ic.connect(:test_signal) { |t| @val1 = t }
137
+ InstanceClassTest.connect(:test_signal) { |t| @val2 = t}
138
+ ic.fire_test_sig(1)
139
+ assert_equal(1, @val1)
140
+ assert_nil(@val2)
141
+ InstanceClassTest.fire_class_test_sig(2)
142
+ assert_equal(1, @val1)
143
+ assert_nil(@val2)
144
+ end
145
+
146
+ class SpecialInstanceClassTest
147
+ include GBS::SignalSlot
148
+ signal :test_signal
149
+ class_signal :test_signal
150
+
151
+ def self.fire_test_sig(n)
152
+ test_signal(n)
153
+ end
154
+ def fire_test_sig(n)
155
+ test_signal(n)
156
+ end
157
+ end
158
+
159
+ def test_special_case
160
+ SpecialInstanceClassTest.connect(:test_signal) { |v| @val1 = v }
161
+ s = SpecialInstanceClassTest.new
162
+ s.connect(:test_signal) { |v| @val2 = v }
163
+ SpecialInstanceClassTest.fire_test_sig(10)
164
+ assert_equal(10, @val1)
165
+ assert_nil(@val2)
166
+ s.fire_test_sig(3)
167
+ assert_equal(10, @val1)
168
+ assert_equal(3, @val2)
169
+ end
170
+
171
+ def test_bad_use
172
+ it = InstanceTest.new
173
+ assert_raise(ArgumentError) { it.connect(:test_signal) }
174
+ end
175
+ end
metadata ADDED
@@ -0,0 +1,55 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gbs-signal-slot
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Greenblack Software
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-04-06 00:00:00 +02:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description:
17
+ email: info@greenblacksoftware.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README
24
+ files:
25
+ - test/tc_gbs_signal_slot.rb
26
+ - lib/gbs_signal_slot.rb
27
+ - README
28
+ has_rdoc: true
29
+ homepage: www.greenblacksoftware.com
30
+ post_install_message:
31
+ rdoc_options: []
32
+
33
+ require_paths:
34
+ - lib
35
+ required_ruby_version: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: "0"
40
+ version:
41
+ required_rubygems_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: "0"
46
+ version:
47
+ requirements: []
48
+
49
+ rubyforge_project: gbs-signal-slot
50
+ rubygems_version: 1.1.0
51
+ signing_key:
52
+ specification_version: 2
53
+ summary: GBS::SignalSlot - a signal/slot mechanism in Ruby
54
+ test_files:
55
+ - test/tc_gbs_signal_slot.rb