rubactive 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 +8 -0
- data/.rvmrc +2 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +283 -0
- data/Rakefile +13 -0
- data/lib/rubactive.rb +2 -0
- data/lib/rubactive/rubactive.rb +254 -0
- data/lib/rubactive/version.rb +3 -0
- data/rubactive.gemspec +29 -0
- data/test/rubactive-end-to-end-ish-tests.rb +53 -0
- data/test/rubactive-tests.rb +244 -0
- data/test/testutil.rb +18 -0
- data/test/testutil/mock-talk.rb +76 -0
- metadata +130 -0
data/.gitignore
ADDED
data/.rvmrc
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright (c) 2012 Brian Marick
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
= Rubactive
|
|
2
|
+
|
|
3
|
+
I had a hard time figuring out what "functional reactive
|
|
4
|
+
programming" and "reactive programming" meant from
|
|
5
|
+
documentation on the web. Implementing it helped. This is
|
|
6
|
+
the implementation. You can tell me if I'm still confused.
|
|
7
|
+
|
|
8
|
+
Note: a big part of my confusion was because, I believe, the
|
|
9
|
+
terminology used obscures more than it reveals, so I tried
|
|
10
|
+
to pick better names. Again, you'll be the judge of whether
|
|
11
|
+
I succeeded.
|
|
12
|
+
|
|
13
|
+
The implementation I chose is most closely modeled after
|
|
14
|
+
Flapjax (http://www.flapjax-lang.org/). Their tutorial is
|
|
15
|
+
pretty good, though it uses the conventional names and is
|
|
16
|
+
occasionally obfuscated by having flapjax code mixed up with
|
|
17
|
+
HTML. Still: good job, guys!
|
|
18
|
+
|
|
19
|
+
Note: my implementation is extremely naive (for example, it
|
|
20
|
+
doesn't even try to deal with "glitching"), and it's missing
|
|
21
|
+
some of the nice utility functions Flapjax has.
|
|
22
|
+
|
|
23
|
+
= Rubactive
|
|
24
|
+
|
|
25
|
+
To use Rubactive, you'll need Ruby 1.9. (I use 1.9.2.) Do
|
|
26
|
+
this in irb:
|
|
27
|
+
|
|
28
|
+
require 'rubactive'
|
|
29
|
+
include Rubactive
|
|
30
|
+
|
|
31
|
+
The idea of reactive programming is that you have values
|
|
32
|
+
that change as a reaction to changes in other values. There
|
|
33
|
+
are two ways to look at such values:
|
|
34
|
+
|
|
35
|
+
[time-varying values]
|
|
36
|
+
There's a single value that changes over time. It might
|
|
37
|
+
be 5 at one moment and 6 at another. That might happen
|
|
38
|
+
because you explicitly changed it, but it might also
|
|
39
|
+
change because *some* *other* time-varying value changed
|
|
40
|
+
and this one reacted to that.
|
|
41
|
+
|
|
42
|
+
In Rubactive, such values are of class Rubactive::TimeVaryingValue.
|
|
43
|
+
|
|
44
|
+
[streams of values]
|
|
45
|
+
Instead of one value that changes, it can be convenient
|
|
46
|
+
to think of a stream of distinct values, arriving one at
|
|
47
|
+
a time. A stream might change because some code added a
|
|
48
|
+
value to it, or because it reacted to a new value
|
|
49
|
+
appearing on another stream.
|
|
50
|
+
|
|
51
|
+
In Rubactive, such streams are of class
|
|
52
|
+
Rubactive::DiscreteValueStream.
|
|
53
|
+
|
|
54
|
+
I used terminology like "a way to look at" and "convenient
|
|
55
|
+
to think of" because there's no huge difference between the
|
|
56
|
+
two classes. They are both thin wrappers (that mainly
|
|
57
|
+
provide different terminology) over a base ReactiveNode
|
|
58
|
+
class (which I haven't bothered to document).
|
|
59
|
+
|
|
60
|
+
== TimeVaryingValues
|
|
61
|
+
|
|
62
|
+
The simple way to create a time-varying value is to give it
|
|
63
|
+
a starting value:
|
|
64
|
+
|
|
65
|
+
origin = TimeVaryingValue.starting_with(0)
|
|
66
|
+
|
|
67
|
+
The current value of +origin+ can be found like this:
|
|
68
|
+
|
|
69
|
+
origin.current #=> 0
|
|
70
|
+
|
|
71
|
+
(Note: it's sort of lame that we refer to +origin+ as a
|
|
72
|
+
value but have to use +current+ to get the... value... of
|
|
73
|
+
the... value. Some reactive frameworks work to hide the fact
|
|
74
|
+
that +origin+ isn't really a value, but rather an
|
|
75
|
+
object-containing-a-value. I don't do that.)
|
|
76
|
+
|
|
77
|
+
We can also create another time-varying value that will
|
|
78
|
+
always be the same as +origin+:
|
|
79
|
+
|
|
80
|
+
exactly = TimeVaryingValue.follows(origin)
|
|
81
|
+
exactly.current #=> 0
|
|
82
|
+
|
|
83
|
+
Here's a way to see that +exactly+ really does follow
|
|
84
|
+
+origin+:
|
|
85
|
+
|
|
86
|
+
origin.change_to("dawn!")
|
|
87
|
+
exactly.current #=> "dawn!"
|
|
88
|
+
|
|
89
|
+
That's not wildly exciting, so let's have one time-varying
|
|
90
|
+
value be a function of another:
|
|
91
|
+
|
|
92
|
+
upper = TimeVaryingValue.follows(origin) { | o | o.upcase }
|
|
93
|
+
upper.current #=> "DAWN!"
|
|
94
|
+
origin.change_to("dawn, paul, and sophie")
|
|
95
|
+
upper.current #=> "DAWN, PAUL, AND SOPHIE"
|
|
96
|
+
|
|
97
|
+
(As noted before, it'd be better if time-varying values
|
|
98
|
+
looked like integers, or strings, or whatever, instead of
|
|
99
|
+
objects containing integers, or strings, or whatever. As a
|
|
100
|
+
gesture toward that, I made it so +method_missing+
|
|
101
|
+
constructed new time-varying-values:
|
|
102
|
+
|
|
103
|
+
coolness = origin.upcase
|
|
104
|
+
coolness.current # => "DAWN, PAUL, AND SOPHIE"
|
|
105
|
+
origin.change_to("your name here")
|
|
106
|
+
coolness.current # => "YOUR NAME HERE"
|
|
107
|
+
|
|
108
|
+
That really doesn't add anything to your understanding, but
|
|
109
|
+
what's the point of programming in Ruby if you can't show
|
|
110
|
+
off?)
|
|
111
|
+
|
|
112
|
+
There's no reason why time-varying values can't be dependent
|
|
113
|
+
on more than one "origin":
|
|
114
|
+
|
|
115
|
+
annoyance = TimeVaryingValue.starting_with(" [that's what she said]")
|
|
116
|
+
michael = coolness + annoyance
|
|
117
|
+
# above shorthand equivalent to:
|
|
118
|
+
# michael = TimeVaryingValue.follows(coolness, annoyance) do | c, a |
|
|
119
|
+
# c + a
|
|
120
|
+
# end
|
|
121
|
+
michael.current #=> "YOUR NAME HERE [that's what she said]"
|
|
122
|
+
|
|
123
|
+
annoyance.change_to(" in bed!")
|
|
124
|
+
michael.current #=> "YOUR NAME HERE in bed!"
|
|
125
|
+
|
|
126
|
+
== DiscreteValueStream
|
|
127
|
+
|
|
128
|
+
Now let's consider a stream of values, where a new value
|
|
129
|
+
might appear at any instant. (You can probably see how this
|
|
130
|
+
might be useful for modeling user input.) Here's how to
|
|
131
|
+
create a stream that doesn't depend on anything:
|
|
132
|
+
|
|
133
|
+
values = DiscreteValueStream.manual
|
|
134
|
+
|
|
135
|
+
You can put something onto a stream and look at it:
|
|
136
|
+
|
|
137
|
+
values.add_value(5)
|
|
138
|
+
values.most_recent_value #=> 5
|
|
139
|
+
|
|
140
|
+
As you might expect, you can have one stream follow another:
|
|
141
|
+
|
|
142
|
+
boring_values = DiscreteValueStream.manual
|
|
143
|
+
excited_values = DiscreteValueStream.follows(boring_values) do | b |
|
|
144
|
+
b.upcase + "!"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
boring_values.add_value("party")
|
|
148
|
+
excited_values.most_recent_value #=> "PARTY!"
|
|
149
|
+
|
|
150
|
+
== The outside world
|
|
151
|
+
|
|
152
|
+
The "reactive world" is one in which values are tied
|
|
153
|
+
together with relationships created by +follows+. But that
|
|
154
|
+
reactive world is embedded within other code that's not
|
|
155
|
+
reactive. For example, it might be that a change to a
|
|
156
|
+
reactive value should make a user interface control update.
|
|
157
|
+
|
|
158
|
+
This can be done by handing a callback to the reactive
|
|
159
|
+
value. Here's how a new addition to a value stream can
|
|
160
|
+
affect the non-reactive world:
|
|
161
|
+
|
|
162
|
+
excited_values.on_addition do | most_recent |
|
|
163
|
+
puts "This new value has been added: #{most_recent}"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
boring_values.add_value("vegetate")
|
|
167
|
+
# This new value has been added: VEGETATE!
|
|
168
|
+
|
|
169
|
+
The same can be done with time-varying-values, but the
|
|
170
|
+
method name is different (for clarity):
|
|
171
|
+
|
|
172
|
+
tvv = TimeVaryingValue.starting_with(8)
|
|
173
|
+
tvv.on_change do | current |
|
|
174
|
+
puts "New value: #{current.inspect}"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
tvv.change_to("Veterinarians >> human medicine people")
|
|
178
|
+
New value: "Veterinarians >> human medicine people"
|
|
179
|
+
|
|
180
|
+
== An end-to-end-example
|
|
181
|
+
|
|
182
|
+
Consider a model-view-controller architecture that lets a
|
|
183
|
+
user control a particular hardware setting. The user
|
|
184
|
+
interface displays the current setting, and provides
|
|
185
|
+
controls that lets the user change it by some delta. In a
|
|
186
|
+
typical MVC implementation, the controller takes an active
|
|
187
|
+
role in shuttling events and values between layers of the
|
|
188
|
+
system. But that responsibility could be implemented
|
|
189
|
+
declaratively with reactive values.
|
|
190
|
+
|
|
191
|
+
Let's begin!
|
|
192
|
+
|
|
193
|
+
The current hardware setting is a time-varying value:
|
|
194
|
+
|
|
195
|
+
hardware_setting = TimeVaryingValue.starting_with(50)
|
|
196
|
+
|
|
197
|
+
The user's actions can be considered to be a stream of delta
|
|
198
|
+
values:
|
|
199
|
+
|
|
200
|
+
deltas = DiscreteValueStream.manual
|
|
201
|
+
|
|
202
|
+
That stream of deltas should be combined with the hardware
|
|
203
|
+
setting to produce a stream of desired settings:
|
|
204
|
+
|
|
205
|
+
user_changes = DiscreteValueStream.follows(deltas) do |delta|
|
|
206
|
+
delta + hardware_setting.current
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
(Alternately, we could be more terse:
|
|
210
|
+
|
|
211
|
+
user_changes = deltas + hardware_setting.current
|
|
212
|
+
|
|
213
|
+
... but that would be showing off.)
|
|
214
|
+
|
|
215
|
+
Note: I'm not having the +user_changes+ follow the hardware
|
|
216
|
+
settings because independent changes to the hardware don't
|
|
217
|
+
count as *user* changes.
|
|
218
|
+
|
|
219
|
+
When a user asks for a change, the hardware should be told
|
|
220
|
+
to change. That steps out of the reactive framework, so it
|
|
221
|
+
requires a callback. I'm going to pretend that the callback
|
|
222
|
+
does various things, one of which is to change the
|
|
223
|
+
+hardware_setting+ value:
|
|
224
|
+
|
|
225
|
+
user_changes.on_addition do |value|
|
|
226
|
+
# The code talks to the real hardware and also sets the authoritative value:
|
|
227
|
+
hardware_setting.change_to(value)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
At this point, we've propagated the user's desires
|
|
231
|
+
"downward" (toward the hardware), but we also have to
|
|
232
|
+
propagate the truth about the hardware upward (toward the
|
|
233
|
+
user interface). We could have the user interface directly
|
|
234
|
+
reflect the low-level +hardware_setting+ value, but lets
|
|
235
|
+
decouple things a bit by having the displayed value +follow+
|
|
236
|
+
the +hardware_setting+:
|
|
237
|
+
|
|
238
|
+
value_displayed = TimeVaryingValue.follows(hardware_setting)
|
|
239
|
+
|
|
240
|
+
(There'd presumably be some sort of +on_change+ callback to
|
|
241
|
+
communicate changes to the time-varying value to the
|
|
242
|
+
user-interface control.)
|
|
243
|
+
|
|
244
|
+
So, now that we've done this wiring, how does it work?
|
|
245
|
+
|
|
246
|
+
The hardware setting starts at 50, because we told it that's
|
|
247
|
+
the default:
|
|
248
|
+
|
|
249
|
+
hardware_setting.current #=> 50
|
|
250
|
+
|
|
251
|
+
Because the displayed value follows the hardware setting, it
|
|
252
|
+
too is 50:
|
|
253
|
+
|
|
254
|
+
value_displayed.current #=> 50
|
|
255
|
+
|
|
256
|
+
Suppose the user clicks a button, enters a text value, or drags
|
|
257
|
+
a slider---whatever. That provokes code that adds a value to
|
|
258
|
+
the +deltas+ stream. Let's simulate that:
|
|
259
|
+
|
|
260
|
+
deltas.add_value(5)
|
|
261
|
+
|
|
262
|
+
Did that count as a new +user_change+? Yes:
|
|
263
|
+
|
|
264
|
+
user_changes.most_recent_value #=> 55
|
|
265
|
+
|
|
266
|
+
Did that (via the callback) change the value of the
|
|
267
|
+
hardware setting? Yes:
|
|
268
|
+
|
|
269
|
+
hardware_setting.current #=> 55
|
|
270
|
+
|
|
271
|
+
Was that value reflected up to the user interface? Yes:
|
|
272
|
+
|
|
273
|
+
value_displayed.current #=> 55
|
|
274
|
+
|
|
275
|
+
All this was done declaratively (with a sort of DSL), rather
|
|
276
|
+
than with writing controller methods. That's the promise of
|
|
277
|
+
reactive programming.
|
|
278
|
+
|
|
279
|
+
== Copyright
|
|
280
|
+
|
|
281
|
+
Copyright (c) 2012 Brian Marick. See LICENSE.txt for
|
|
282
|
+
further details.
|
|
283
|
+
|
data/Rakefile
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
require "bundler/gem_tasks"
|
|
2
|
+
|
|
3
|
+
require 'rake/testtask'
|
|
4
|
+
Rake::TestTask.new(:test) do |test|
|
|
5
|
+
test.libs << 'lib' << 'test'
|
|
6
|
+
test.pattern = 'test/**/*test*.rb'
|
|
7
|
+
test.verbose = true
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
task :default => :test
|
|
11
|
+
|
|
12
|
+
task :rdoc
|
|
13
|
+
`rdoc README.rdoc lib`
|
data/lib/rubactive.rb
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
require 'forwardable'
|
|
2
|
+
|
|
3
|
+
# Rubactive provides two kinds of values-that-react-to-changes-in-other-values.
|
|
4
|
+
#
|
|
5
|
+
# Sometimes you want to think of the changing value as being something
|
|
6
|
+
# that "magically" changes over time (typically in reaction to changes
|
|
7
|
+
# in other values). That perspective is implemented by TimeVaryingValue.
|
|
8
|
+
#
|
|
9
|
+
# Sometimes you want to think of a stream of unchanging values that
|
|
10
|
+
# "magically" arrive (often in reaction to values appearing in other
|
|
11
|
+
# streams). That perspective is implemented by DiscreteValueStream.
|
|
12
|
+
#
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
module Rubactive
|
|
16
|
+
class ReactiveNode # :nodoc:
|
|
17
|
+
# This should probably be a delegate instead of superclass
|
|
18
|
+
# because the subclasses don't want all of the methods
|
|
19
|
+
# (but the tests do)
|
|
20
|
+
private_class_method :new
|
|
21
|
+
|
|
22
|
+
def self.follows(*earlier_nodes, &updater)
|
|
23
|
+
new(*earlier_nodes, &updater)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
attr_reader :value
|
|
27
|
+
|
|
28
|
+
DEFAULT_VALUE = :no_value_at_all
|
|
29
|
+
|
|
30
|
+
def initialize(*earlier_nodes, &recalculator)
|
|
31
|
+
@value = DEFAULT_VALUE
|
|
32
|
+
@recalculator = recalculator || ->val {val}
|
|
33
|
+
@later_nodes = []
|
|
34
|
+
@earlier_nodes = earlier_nodes
|
|
35
|
+
@change_callback = ->ignored{}
|
|
36
|
+
tell_earlier_nodes_about_me(earlier_nodes)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def tell_earlier_nodes_about_me(earlier_nodes)
|
|
40
|
+
earlier_nodes.each do |e|
|
|
41
|
+
e.this_node_is_later_than_you(self) if e.is_a?(ReactiveNode)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def this_node_is_later_than_you(this_node)
|
|
46
|
+
@later_nodes << this_node
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def recalculate
|
|
50
|
+
propagate(@recalculator.call(*just_values(@earlier_nodes)))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def value=(new_value)
|
|
54
|
+
propagate(new_value)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# When the value of this variable changes, call the block argument.
|
|
58
|
+
def on_change(&block)
|
|
59
|
+
@change_callback = block
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def propagate(value)
|
|
63
|
+
@value = value
|
|
64
|
+
@change_callback.(value)
|
|
65
|
+
@later_nodes.each do |node|
|
|
66
|
+
node.recalculate
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def method_missing(message, *args)
|
|
71
|
+
recalculator = lambda do |*just_values|
|
|
72
|
+
receiver = just_values.shift
|
|
73
|
+
receiver.send(message, *just_values)
|
|
74
|
+
end
|
|
75
|
+
self.class.follows(self, *args, &recalculator)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def just_values(args)
|
|
79
|
+
args.collect do |arg|
|
|
80
|
+
if arg.is_a?(ReactiveNode)
|
|
81
|
+
arg.value
|
|
82
|
+
else
|
|
83
|
+
arg
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# test_support
|
|
89
|
+
|
|
90
|
+
def self.blank
|
|
91
|
+
follows() {}
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# A TimeVaryingValue represents a value that might "mysteriously"
|
|
97
|
+
# change each time you look at it.
|
|
98
|
+
#
|
|
99
|
+
# The current value can change in three ways:
|
|
100
|
+
#
|
|
101
|
+
# * Explicitly, via change_to. That's no different than setting an attribute of any
|
|
102
|
+
# sort of object.
|
|
103
|
+
#
|
|
104
|
+
# * It might have been created to "follow" other time-varying values. In that case,
|
|
105
|
+
# it will react to any of their changes by recalculating itself (in a way
|
|
106
|
+
# defined when it was created.)
|
|
107
|
+
#
|
|
108
|
+
# * It might have been created to follow a DiscreteValueStream. In that case, any
|
|
109
|
+
# value added to the stream becomes the time-varying value's current value.
|
|
110
|
+
#
|
|
111
|
+
# There is a different constructor for each of the above cases. In addition, variables
|
|
112
|
+
# can be implicity created by sending unrecognized messages to other time-varying values.
|
|
113
|
+
#
|
|
114
|
+
# origin = TimeVaryingValue.starting_with(5)
|
|
115
|
+
# follower = origin + 1
|
|
116
|
+
# follower.current #=> 6
|
|
117
|
+
#
|
|
118
|
+
# origin.change_to(700)
|
|
119
|
+
# follower.current #=> 701
|
|
120
|
+
class TimeVaryingValue < ReactiveNode
|
|
121
|
+
# Create a value that changes as the values it depends upon change.
|
|
122
|
+
#
|
|
123
|
+
# When any followed variables change, the block is called with their current
|
|
124
|
+
# values. The result becomes the current value for this TimeVaryingValue.
|
|
125
|
+
#
|
|
126
|
+
# Example:
|
|
127
|
+
# verb = TimeVaryingValue.starting_with("vote")
|
|
128
|
+
# adverb = TimeVaryingValue.starting_with("early")
|
|
129
|
+
# tracker = TimeVaryingValue.follows(verb, adverb) { | v, a | "#{v} #{a}!" }
|
|
130
|
+
# tracker.current #=> "vote early!"
|
|
131
|
+
#
|
|
132
|
+
# adverb.change_to("often")
|
|
133
|
+
# tracker.current #=> "vote often!"
|
|
134
|
+
#
|
|
135
|
+
# If no block is given, this value should be following only one other.
|
|
136
|
+
# It adopts that other value whenever it changes.
|
|
137
|
+
#--
|
|
138
|
+
# Defined here so that I can write special-purpose documentation.
|
|
139
|
+
def self.follows(*values, &block)
|
|
140
|
+
super
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Create a new TimeVaryingValue.
|
|
144
|
+
#
|
|
145
|
+
# Since the returned instance follows nothing, only change_to can
|
|
146
|
+
# be used to change its value.
|
|
147
|
+
def self.starting_with(initial_value)
|
|
148
|
+
follows { initial_value }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Create a time-varying value that follows the latest value in a stream.
|
|
152
|
+
#
|
|
153
|
+
# The first argument must be a DiscreteValueStream. Whenever that stream
|
|
154
|
+
# has a new value added, the time-varying value changes to match.
|
|
155
|
+
#
|
|
156
|
+
# The time-varying value always starts with the given initial_value, even
|
|
157
|
+
# if stream has previously had some values added to it.
|
|
158
|
+
#--
|
|
159
|
+
# Flapjax startsWith
|
|
160
|
+
def self.tracks_stream(value_stream, initial_value)
|
|
161
|
+
retval = follows(value_stream)
|
|
162
|
+
retval.change_to(initial_value)
|
|
163
|
+
retval
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Retrieve the current value.
|
|
167
|
+
#
|
|
168
|
+
# Earlier values are inaccessible.
|
|
169
|
+
def current; value; end
|
|
170
|
+
|
|
171
|
+
# Change the current value.
|
|
172
|
+
def change_to(new_value); self.value=new_value; end
|
|
173
|
+
|
|
174
|
+
def initialize(*earlier_nodes, &recalculator) # :nodoc:
|
|
175
|
+
super
|
|
176
|
+
recalculate
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# A DiscreteValueStream represents a stream of values. When a value is added to the stream,
|
|
182
|
+
# other DiscreteValueStreams may react if they follow this stream.
|
|
183
|
+
#
|
|
184
|
+
# Streams are explicitly created with DiscreteValueStream.manual or
|
|
185
|
+
# DiscreteValueStream.follows. In the first case, values are added
|
|
186
|
+
# only with add_value. In the second, values can also be added in
|
|
187
|
+
# reaction to the streams being followed.
|
|
188
|
+
#
|
|
189
|
+
# Streams can also be implicity created by sending other streams unrecognized messages.
|
|
190
|
+
#
|
|
191
|
+
# origin = DiscreteValueStream.manual
|
|
192
|
+
# follower = origin + 1
|
|
193
|
+
#
|
|
194
|
+
# The previous definition of the follower stream is equivalent to this:
|
|
195
|
+
#
|
|
196
|
+
# follower = DiscreteValueStream.follows(origin) { | o | o + 1 }
|
|
197
|
+
class DiscreteValueStream < ReactiveNode
|
|
198
|
+
# Create a stream that reacts to one or more other streams.
|
|
199
|
+
#
|
|
200
|
+
# The addition of values to any of the streams will cause the block
|
|
201
|
+
# to be called with their most recent values. The result is added to
|
|
202
|
+
# this stream (as with add_value).
|
|
203
|
+
#
|
|
204
|
+
# Example:
|
|
205
|
+
# origin = DiscreteValueStream.manual
|
|
206
|
+
# follower = DiscreteValueStream.follows(origin) { | o | o+1 }
|
|
207
|
+
# origin.add_value(5)
|
|
208
|
+
# follower.most_recent_value #=> 6
|
|
209
|
+
#
|
|
210
|
+
# If no block is given, this stream should be following only one other stream.
|
|
211
|
+
# The value just added to that stream is also added to this one.
|
|
212
|
+
#--
|
|
213
|
+
# Defined here so that I can write special-purpose documentation.
|
|
214
|
+
def self.follows(*streams, &block)
|
|
215
|
+
super
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Create an empty value stream
|
|
219
|
+
#
|
|
220
|
+
# Use add_value to insert values into the stream.
|
|
221
|
+
def self.manual
|
|
222
|
+
follows {
|
|
223
|
+
raise "Incorrect use of recalculation in a manual event stream"
|
|
224
|
+
}
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Run a callback when a new value is added.
|
|
228
|
+
#
|
|
229
|
+
# This is an interface to the non-reactive world. When a new value is
|
|
230
|
+
# added (whether with add_value or in reaction to a followed stream),
|
|
231
|
+
# the callback is called and given that value. The callback will typically
|
|
232
|
+
# do something with the value, like add it to a GUI.
|
|
233
|
+
def on_addition(&callback); on_change(&callback); end
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# Retrieve last value added to the stream
|
|
237
|
+
#
|
|
238
|
+
# Earlier values are inaccessible.
|
|
239
|
+
#
|
|
240
|
+
# It is an error to ask for the value of an #empty? stream.
|
|
241
|
+
def most_recent_value; @value; end
|
|
242
|
+
|
|
243
|
+
# Place a new value on the stream
|
|
244
|
+
def add_value(new_value) # this?
|
|
245
|
+
self.value = new_value
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# True iff no value has ever been added to the stream.
|
|
249
|
+
def empty?
|
|
250
|
+
most_recent_value == DEFAULT_VALUE
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
data/rubactive.gemspec
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
|
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
|
3
|
+
require "rubactive/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |s|
|
|
6
|
+
s.name = "rubactive"
|
|
7
|
+
s.homepage = "http://github.com/marick/rubactive"
|
|
8
|
+
s.license = "MIT"
|
|
9
|
+
s.summary = %Q{A basic and perhaps misinformed library for learning about reactive programming}
|
|
10
|
+
s.description = %Q{A basic and perhaps misinformed library for learning about reactive programming}
|
|
11
|
+
s.email = "marick@exampler.com"
|
|
12
|
+
s.authors = ["Brian Marick"]
|
|
13
|
+
s.required_ruby_version = '>= 1.9.2'
|
|
14
|
+
s.version = Rubactive::VERSION
|
|
15
|
+
|
|
16
|
+
s.rubyforge_project = "rubactive"
|
|
17
|
+
|
|
18
|
+
s.files = `git ls-files`.split("\n")
|
|
19
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
|
20
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
|
21
|
+
s.require_paths = ["lib"]
|
|
22
|
+
|
|
23
|
+
s.add_development_dependency "shoulda", ">= 0"
|
|
24
|
+
s.add_development_dependency "assert2"
|
|
25
|
+
s.add_development_dependency "rr"
|
|
26
|
+
s.add_development_dependency "bundler", "~> 1.0.0"
|
|
27
|
+
s.add_development_dependency "jeweler", "~> 1.6.4"
|
|
28
|
+
s.add_development_dependency "test-unit"
|
|
29
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
require 'rubactive'
|
|
2
|
+
require_relative 'testutil'
|
|
3
|
+
|
|
4
|
+
class RubactiveUseCaseTests < Test::Unit::TestCase
|
|
5
|
+
include Rubactive
|
|
6
|
+
|
|
7
|
+
MIN=0
|
|
8
|
+
MAX=100
|
|
9
|
+
DEFAULT=50
|
|
10
|
+
|
|
11
|
+
def setup
|
|
12
|
+
# What our program knows about the current setting.
|
|
13
|
+
@hardware_setting = TimeVaryingValue.starting_with(DEFAULT)
|
|
14
|
+
|
|
15
|
+
# User-initiated deltas to the hardware setting
|
|
16
|
+
@deltas = DiscreteValueStream.manual
|
|
17
|
+
|
|
18
|
+
# Convert a stream of deltas to a stream of actual values
|
|
19
|
+
@user_changes = DiscreteValueStream.follows(@deltas) do |delta|
|
|
20
|
+
delta + @hardware_setting.current
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Callback to code that manipulates hardware.
|
|
24
|
+
@user_changes.on_addition do |value|
|
|
25
|
+
# The code talks to the real hardware and also sets the authoritative value:
|
|
26
|
+
@hardware_setting.change_to(value)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
#Pretend this is the value of a slider or something
|
|
30
|
+
@value_displayed = TimeVaryingValue.follows(@hardware_setting)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
should "propagate user changes" do
|
|
34
|
+
@deltas.add_value(5)
|
|
35
|
+
|
|
36
|
+
assert_equal(55, @hardware_setting.current)
|
|
37
|
+
assert_equal(55, @value_displayed.current)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
should "propagate and obey hardware changes" do
|
|
41
|
+
@hardware_setting.change_to(80)
|
|
42
|
+
|
|
43
|
+
assert_equal(80, @hardware_setting.current)
|
|
44
|
+
assert_equal(80, @value_displayed.current)
|
|
45
|
+
|
|
46
|
+
@deltas.add_value(5)
|
|
47
|
+
|
|
48
|
+
assert_equal(85, @hardware_setting.current)
|
|
49
|
+
assert_equal(85, @value_displayed.current)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
require 'rubactive'
|
|
2
|
+
require_relative 'testutil'
|
|
3
|
+
|
|
4
|
+
class Fixnum
|
|
5
|
+
# A useful N-argument method for testing
|
|
6
|
+
def max3(other1, other2)
|
|
7
|
+
[self, other1, other2].max
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RubactiveTests < Test::Unit::TestCase
|
|
13
|
+
include Rubactive
|
|
14
|
+
|
|
15
|
+
context "time-varying values" do
|
|
16
|
+
should "hold values" do
|
|
17
|
+
v = TimeVaryingValue.starting_with(3)
|
|
18
|
+
assert_equal(3, v.current)
|
|
19
|
+
v.change_to(4)
|
|
20
|
+
assert_equal(4, v.current)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
context "following" do
|
|
24
|
+
|
|
25
|
+
should "allow other time-varying values" do
|
|
26
|
+
origin = TimeVaryingValue.starting_with(3)
|
|
27
|
+
destination = TimeVaryingValue.follows(origin) { |o| o + 1}
|
|
28
|
+
assert_equal(4, destination.current)
|
|
29
|
+
origin.change_to(4)
|
|
30
|
+
assert_equal(5, destination.current)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
should "allow chains" do
|
|
34
|
+
origin = TimeVaryingValue.starting_with(3)
|
|
35
|
+
middle = TimeVaryingValue.follows(origin) { |o| o + 1}
|
|
36
|
+
destination = TimeVaryingValue.follows(middle) { |o| o + 1}
|
|
37
|
+
|
|
38
|
+
assert_equal(4, middle.current)
|
|
39
|
+
assert_equal(5, destination.current)
|
|
40
|
+
|
|
41
|
+
origin.change_to(40)
|
|
42
|
+
assert_equal(41, middle.current)
|
|
43
|
+
assert_equal(42, destination.current)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
should "allow discrete value streams" do
|
|
47
|
+
changes = DiscreteValueStream.manual
|
|
48
|
+
tracker = TimeVaryingValue.tracks_stream(changes, 88)
|
|
49
|
+
assert_equal(88, tracker.current)
|
|
50
|
+
changes.add_value(5)
|
|
51
|
+
assert_equal(5, tracker.current)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
context "implicit creation" do
|
|
56
|
+
|
|
57
|
+
should "generates value calculation from method-missing" do
|
|
58
|
+
origin = TimeVaryingValue.starting_with(8)
|
|
59
|
+
destination = origin + 1
|
|
60
|
+
assert_equal(9, destination.current)
|
|
61
|
+
assert_equal(TimeVaryingValue, destination.class)
|
|
62
|
+
|
|
63
|
+
origin.change_to(33)
|
|
64
|
+
assert_equal(34, destination.current)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
should "work with multi-argument methods" do
|
|
68
|
+
assert_equal(3, 1.max3(2, 3))
|
|
69
|
+
|
|
70
|
+
origin = TimeVaryingValue.starting_with(2)
|
|
71
|
+
other = origin * -1
|
|
72
|
+
final = origin.max3(8, other)
|
|
73
|
+
assert_equal(8, final.current)
|
|
74
|
+
|
|
75
|
+
origin.change_to(100)
|
|
76
|
+
assert_equal(100, final.current)
|
|
77
|
+
|
|
78
|
+
origin.change_to(-222)
|
|
79
|
+
assert_equal(222, final.current)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
context "discrete value streams" do
|
|
85
|
+
should "remember their most recent value" do
|
|
86
|
+
s = DiscreteValueStream.manual
|
|
87
|
+
s.add_value(33)
|
|
88
|
+
assert_equal(33, s.most_recent_value)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
should "start out with a null-like value" do
|
|
92
|
+
s = DiscreteValueStream.manual
|
|
93
|
+
assert_equal(:no_value_at_all, s.most_recent_value)
|
|
94
|
+
assert_true(true, s.empty?)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
should "be able to create new event streams from old" do
|
|
98
|
+
stream = DiscreteValueStream.manual
|
|
99
|
+
transformed = DiscreteValueStream.follows(stream) { |s|
|
|
100
|
+
s + 1
|
|
101
|
+
}
|
|
102
|
+
stream.add_value(33)
|
|
103
|
+
assert_equal(34, transformed.most_recent_value)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
should "be able to create streams implicitly" do
|
|
107
|
+
stream = DiscreteValueStream.manual
|
|
108
|
+
transformed = stream + 1
|
|
109
|
+
assert_equal(DiscreteValueStream, transformed.class)
|
|
110
|
+
|
|
111
|
+
stream.add_value(33)
|
|
112
|
+
assert_equal(34, transformed.most_recent_value)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
### Behavior common to both types of value-holders
|
|
117
|
+
### Not a public API
|
|
118
|
+
|
|
119
|
+
context "Reactive Nodes" do
|
|
120
|
+
should "take a block that calculates their value" do
|
|
121
|
+
n = ReactiveNode.blank
|
|
122
|
+
n.value=5
|
|
123
|
+
assert_equal(5, n.value)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
should "be able to depend on other nodes" do
|
|
127
|
+
before = ReactiveNode.blank
|
|
128
|
+
before.value = 5
|
|
129
|
+
|
|
130
|
+
after = ReactiveNode.follows(before) do |b|
|
|
131
|
+
1 + b
|
|
132
|
+
end
|
|
133
|
+
after.recalculate
|
|
134
|
+
assert_equal(6, after.value)
|
|
135
|
+
|
|
136
|
+
before.value = 88
|
|
137
|
+
assert_equal(89, after.value)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
should "be able to depend on a combination of nodes" do
|
|
141
|
+
a_node = ReactiveNode.blank
|
|
142
|
+
a_node.value = 1
|
|
143
|
+
|
|
144
|
+
b_node = ReactiveNode.blank
|
|
145
|
+
b_node.value = 10
|
|
146
|
+
|
|
147
|
+
captured = 100
|
|
148
|
+
|
|
149
|
+
combiner = ReactiveNode.follows(a_node, b_node) do |a,b|
|
|
150
|
+
a+b+captured
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
combiner.recalculate
|
|
154
|
+
assert_equal(111, combiner.value)
|
|
155
|
+
|
|
156
|
+
a_node.value = 20000
|
|
157
|
+
assert_equal(20110, combiner.value)
|
|
158
|
+
|
|
159
|
+
# unsurprisingly, changing the plain variable triggers no propagation
|
|
160
|
+
captured = -a_node.value
|
|
161
|
+
assert_equal(20110, combiner.value)
|
|
162
|
+
|
|
163
|
+
b_node.value = 88
|
|
164
|
+
assert_equal(88, combiner.value)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
should "follow a variable if no block given" do
|
|
168
|
+
before = ReactiveNode.blank
|
|
169
|
+
after = ReactiveNode.follows(before)
|
|
170
|
+
before.value = 88
|
|
171
|
+
assert_equal(88, after.value)
|
|
172
|
+
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
should "be able to generate nodes implicitly" do
|
|
176
|
+
origin = ReactiveNode.blank
|
|
177
|
+
origin.value = 8
|
|
178
|
+
destination = origin + 1
|
|
179
|
+
destination.recalculate
|
|
180
|
+
assert_equal(9, destination.value)
|
|
181
|
+
|
|
182
|
+
origin.value=33
|
|
183
|
+
assert_equal(34, destination.value)
|
|
184
|
+
|
|
185
|
+
final_destination = destination + origin
|
|
186
|
+
final_destination.recalculate
|
|
187
|
+
assert_equal(67, final_destination.value)
|
|
188
|
+
|
|
189
|
+
origin.value=1
|
|
190
|
+
assert_equal(2, destination.value)
|
|
191
|
+
assert_equal(3, final_destination.value)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
should "capture many types of value-containing-things" do
|
|
195
|
+
origin = ReactiveNode.blank
|
|
196
|
+
other = ReactiveNode.blank
|
|
197
|
+
other.value = 10
|
|
198
|
+
|
|
199
|
+
captured = 100
|
|
200
|
+
destination = origin + captured + other + 1
|
|
201
|
+
|
|
202
|
+
origin.value=1000
|
|
203
|
+
assert_equal(1111, destination.value)
|
|
204
|
+
|
|
205
|
+
captured = 999999 # This does not trigger update - no surprise
|
|
206
|
+
assert_equal(1111, destination.value)
|
|
207
|
+
|
|
208
|
+
# But it also has no effect on any future updates.
|
|
209
|
+
origin.value=2000
|
|
210
|
+
assert_equal(2111, destination.value)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
context "callbacks to update the outside world" do
|
|
214
|
+
|
|
215
|
+
should "happen on recalculation" do
|
|
216
|
+
origin = ReactiveNode.blank
|
|
217
|
+
destination = ReactiveNode.follows(origin) {|o| o.to_s.upcase.to_sym}
|
|
218
|
+
|
|
219
|
+
triggered = false
|
|
220
|
+
destination.on_change do | value |
|
|
221
|
+
triggered = value
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
origin.value = :new_value
|
|
225
|
+
|
|
226
|
+
assert_equal(:NEW_VALUE, triggered)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
should "happen on value-setting" do
|
|
230
|
+
n = ReactiveNode.blank
|
|
231
|
+
|
|
232
|
+
triggered = false
|
|
233
|
+
n.on_change do | value |
|
|
234
|
+
triggered = value
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
n.value = :new_value
|
|
238
|
+
assert_equal(:new_value, triggered)
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
end
|
data/test/testutil.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
require 'bundler'
|
|
3
|
+
begin
|
|
4
|
+
Bundler.setup(:default, :development)
|
|
5
|
+
rescue Bundler::BundlerError => e
|
|
6
|
+
$stderr.puts e.message
|
|
7
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
|
8
|
+
exit e.status_code
|
|
9
|
+
end
|
|
10
|
+
require 'test/unit'
|
|
11
|
+
require 'shoulda'
|
|
12
|
+
require 'rr'
|
|
13
|
+
|
|
14
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
|
15
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
|
16
|
+
|
|
17
|
+
require_relative 'testutil/mock-talk'
|
|
18
|
+
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
class Test::Unit::TestCase
|
|
2
|
+
include RR::Adapters::TestUnit
|
|
3
|
+
end
|
|
4
|
+
|
|
5
|
+
# These methods are used to change the flow of control so that the
|
|
6
|
+
# test can state what is to happen before stating what the mock should
|
|
7
|
+
# receive.
|
|
8
|
+
class Test::Unit::TestCase
|
|
9
|
+
def during(&block)
|
|
10
|
+
# Use of global needed for current way of handling
|
|
11
|
+
# listeners:
|
|
12
|
+
# listeners_to(@sut).run_methods {...}
|
|
13
|
+
$what_runs_after_mock_setup = block
|
|
14
|
+
self
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def behold!
|
|
18
|
+
yield
|
|
19
|
+
@result = $what_runs_after_mock_setup.call
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def listeners_to(object)
|
|
23
|
+
klass = object.class.const_get("Listener")
|
|
24
|
+
any_old_listener = klass.new
|
|
25
|
+
object.add_listener(any_old_listener)
|
|
26
|
+
|
|
27
|
+
# mock(any_old_listener).increase_setting
|
|
28
|
+
mockable = mock(any_old_listener)
|
|
29
|
+
|
|
30
|
+
def mockable.are_sent(&block)
|
|
31
|
+
instance_eval(&block)
|
|
32
|
+
$what_runs_after_mock_setup.call
|
|
33
|
+
end
|
|
34
|
+
mockable
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def mocked(n = 1)
|
|
38
|
+
if (n == 1)
|
|
39
|
+
one_mock
|
|
40
|
+
else
|
|
41
|
+
(0...n).collect do
|
|
42
|
+
one_mock
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def one_mock
|
|
48
|
+
core_object = Object.new
|
|
49
|
+
mock_wrapper = mock(core_object)
|
|
50
|
+
core_object.define_singleton_method(:is_sent) { mock_wrapper }
|
|
51
|
+
core_object
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def collaborators(*names)
|
|
56
|
+
ensure_strings(names).each do | name |
|
|
57
|
+
result = one_mock()
|
|
58
|
+
self.instance_variable_set(instance_name(name), result)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
alias_method :collaborator, :collaborators
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def ensure_strings(maybe_symbols)
|
|
67
|
+
maybe_symbols.collect(&:to_s)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def instance_name(raw_name)
|
|
71
|
+
"@" + raw_name.to_s
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rubactive
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
prerelease:
|
|
6
|
+
platform: ruby
|
|
7
|
+
authors:
|
|
8
|
+
- Brian Marick
|
|
9
|
+
autorequire:
|
|
10
|
+
bindir: bin
|
|
11
|
+
cert_chain: []
|
|
12
|
+
date: 2012-03-01 00:00:00.000000000Z
|
|
13
|
+
dependencies:
|
|
14
|
+
- !ruby/object:Gem::Dependency
|
|
15
|
+
name: shoulda
|
|
16
|
+
requirement: &2169222580 !ruby/object:Gem::Requirement
|
|
17
|
+
none: false
|
|
18
|
+
requirements:
|
|
19
|
+
- - ! '>='
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '0'
|
|
22
|
+
type: :development
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: *2169222580
|
|
25
|
+
- !ruby/object:Gem::Dependency
|
|
26
|
+
name: assert2
|
|
27
|
+
requirement: &2169222140 !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: *2169222140
|
|
36
|
+
- !ruby/object:Gem::Dependency
|
|
37
|
+
name: rr
|
|
38
|
+
requirement: &2169221560 !ruby/object:Gem::Requirement
|
|
39
|
+
none: false
|
|
40
|
+
requirements:
|
|
41
|
+
- - ! '>='
|
|
42
|
+
- !ruby/object:Gem::Version
|
|
43
|
+
version: '0'
|
|
44
|
+
type: :development
|
|
45
|
+
prerelease: false
|
|
46
|
+
version_requirements: *2169221560
|
|
47
|
+
- !ruby/object:Gem::Dependency
|
|
48
|
+
name: bundler
|
|
49
|
+
requirement: &2169220900 !ruby/object:Gem::Requirement
|
|
50
|
+
none: false
|
|
51
|
+
requirements:
|
|
52
|
+
- - ~>
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: 1.0.0
|
|
55
|
+
type: :development
|
|
56
|
+
prerelease: false
|
|
57
|
+
version_requirements: *2169220900
|
|
58
|
+
- !ruby/object:Gem::Dependency
|
|
59
|
+
name: jeweler
|
|
60
|
+
requirement: &2169220240 !ruby/object:Gem::Requirement
|
|
61
|
+
none: false
|
|
62
|
+
requirements:
|
|
63
|
+
- - ~>
|
|
64
|
+
- !ruby/object:Gem::Version
|
|
65
|
+
version: 1.6.4
|
|
66
|
+
type: :development
|
|
67
|
+
prerelease: false
|
|
68
|
+
version_requirements: *2169220240
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: test-unit
|
|
71
|
+
requirement: &2169219700 !ruby/object:Gem::Requirement
|
|
72
|
+
none: false
|
|
73
|
+
requirements:
|
|
74
|
+
- - ! '>='
|
|
75
|
+
- !ruby/object:Gem::Version
|
|
76
|
+
version: '0'
|
|
77
|
+
type: :development
|
|
78
|
+
prerelease: false
|
|
79
|
+
version_requirements: *2169219700
|
|
80
|
+
description: A basic and perhaps misinformed library for learning about reactive programming
|
|
81
|
+
email: marick@exampler.com
|
|
82
|
+
executables: []
|
|
83
|
+
extensions: []
|
|
84
|
+
extra_rdoc_files: []
|
|
85
|
+
files:
|
|
86
|
+
- .gitignore
|
|
87
|
+
- .rvmrc
|
|
88
|
+
- Gemfile
|
|
89
|
+
- LICENSE.txt
|
|
90
|
+
- README.rdoc
|
|
91
|
+
- Rakefile
|
|
92
|
+
- lib/rubactive.rb
|
|
93
|
+
- lib/rubactive/rubactive.rb
|
|
94
|
+
- lib/rubactive/version.rb
|
|
95
|
+
- rubactive.gemspec
|
|
96
|
+
- test/rubactive-end-to-end-ish-tests.rb
|
|
97
|
+
- test/rubactive-tests.rb
|
|
98
|
+
- test/testutil.rb
|
|
99
|
+
- test/testutil/mock-talk.rb
|
|
100
|
+
homepage: http://github.com/marick/rubactive
|
|
101
|
+
licenses:
|
|
102
|
+
- MIT
|
|
103
|
+
post_install_message:
|
|
104
|
+
rdoc_options: []
|
|
105
|
+
require_paths:
|
|
106
|
+
- lib
|
|
107
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
108
|
+
none: false
|
|
109
|
+
requirements:
|
|
110
|
+
- - ! '>='
|
|
111
|
+
- !ruby/object:Gem::Version
|
|
112
|
+
version: 1.9.2
|
|
113
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
114
|
+
none: false
|
|
115
|
+
requirements:
|
|
116
|
+
- - ! '>='
|
|
117
|
+
- !ruby/object:Gem::Version
|
|
118
|
+
version: '0'
|
|
119
|
+
requirements: []
|
|
120
|
+
rubyforge_project: rubactive
|
|
121
|
+
rubygems_version: 1.8.10
|
|
122
|
+
signing_key:
|
|
123
|
+
specification_version: 3
|
|
124
|
+
summary: A basic and perhaps misinformed library for learning about reactive programming
|
|
125
|
+
test_files:
|
|
126
|
+
- test/rubactive-end-to-end-ish-tests.rb
|
|
127
|
+
- test/rubactive-tests.rb
|
|
128
|
+
- test/testutil.rb
|
|
129
|
+
- test/testutil/mock-talk.rb
|
|
130
|
+
has_rdoc:
|