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