volute 0.1.0 → 0.1.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/CHANGELOG.txt +19 -1
- data/README.rdoc +333 -76
- data/Rakefile +4 -4
- data/TODO.txt +28 -14
- data/examples/diagnosis.rb +82 -0
- data/examples/equation.rb +1 -1
- data/examples/light.rb +27 -0
- data/examples/state_machine.rb +26 -24
- data/examples/state_machine_2.rb +64 -0
- data/examples/traffic.rb +115 -0
- data/lib/volute.rb +159 -32
- data/spec/apply_spec.rb +52 -0
- data/spec/filter_not_spec.rb +58 -0
- data/spec/{volute_guard_spec.rb → filter_spec.rb} +57 -29
- data/spec/filter_state_spec.rb +179 -0
- data/spec/filter_transitions_spec.rb +200 -0
- data/spec/over_spec.rb +36 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/volute_spec.rb +0 -32
- data/spec/volutes_spec.rb +91 -0
- data/volute.gemspec +31 -11
- metadata +60 -20
data/CHANGELOG.txt
CHANGED
@@ -2,5 +2,23 @@
|
|
2
2
|
= volute - CHANGELOG.txt
|
3
3
|
|
4
4
|
|
5
|
-
== volute - 0.1.
|
5
|
+
== volute - 0.1.1 released 2010/10/12
|
6
|
+
|
7
|
+
- volute :att => /regex/ and volute /regex/ => /regex/
|
8
|
+
- volute :attribute => :any / :not_nil { ... }
|
9
|
+
- volute :attribute => [ 'val0', 'val1'] { ... }
|
10
|
+
- volute [ 'prev0', 'prev1' ] => 'current' { ... }
|
11
|
+
- volute 'prev' => [ 'current0', 'current1' ] { ... }
|
12
|
+
- 'state' volutes
|
13
|
+
- Volute.apply(object, attribute=nil, previous_value=nil, value=nil)
|
14
|
+
- volutes /regex_on_attribute_name/ { ... }
|
15
|
+
- volutes :not, args { ... }
|
16
|
+
- volutes.remove(Financing) for removing top-level volutes
|
17
|
+
- volutes(arg) for querying the top-level volutes
|
18
|
+
- enforced guard listing <=> OR and volute nesting <=> AND
|
19
|
+
|
20
|
+
|
21
|
+
== volute - 0.1.0 released 2010/10/07
|
22
|
+
|
23
|
+
- initial release
|
6
24
|
|
data/README.rdoc
CHANGED
@@ -1,139 +1,396 @@
|
|
1
1
|
|
2
2
|
= volute
|
3
3
|
|
4
|
-
|
4
|
+
I wanted to write something about the state of multiple objects, I ended with something that feels like a subset of aspect oriented programming.
|
5
5
|
|
6
6
|
It can be used to implement toy state machines, or dumb rule systems.
|
7
7
|
|
8
|
-
See examples/ and specs/
|
9
8
|
|
9
|
+
== include Volute
|
10
10
|
|
11
|
-
|
11
|
+
When the Volute mixin is included in a class, its attr_accessor call is modified so that the resulting attributer set method, upon setting the value of the attribute triggers a callback defined outside of the class.
|
12
12
|
|
13
|
-
|
13
|
+
require 'rubygems'
|
14
|
+
require 'volute' # gem install volute
|
14
15
|
|
16
|
+
class Light
|
17
|
+
include Volute
|
15
18
|
|
16
|
-
|
19
|
+
attr_accessor :colour
|
20
|
+
attr_accessor :changed_at
|
21
|
+
end
|
17
22
|
|
18
|
-
|
23
|
+
volute :colour do
|
24
|
+
object.changed_at = Time.now
|
25
|
+
end
|
19
26
|
|
20
|
-
|
27
|
+
l = Light.new
|
28
|
+
p l # => #<Light:0x10014c480>
|
21
29
|
|
22
|
-
|
30
|
+
l.colour = :blue
|
31
|
+
p l # => #<Light:0x10014c480 @changed_at=Fri Oct 08 20:01:52 +0900 2010, @colour=:blue>
|
23
32
|
|
24
|
-
|
25
|
-
# our class
|
33
|
+
There is a catch in this example, the volute will trigger for any class that inccludes Volute and which sees a change to its :colour attribute.
|
26
34
|
|
27
|
-
|
35
|
+
Those two classes would see their :colour hooked :
|
36
|
+
|
37
|
+
class Light
|
28
38
|
include Volute
|
29
39
|
|
30
|
-
attr_accessor :
|
40
|
+
attr_accessor :colour
|
41
|
+
attr_accessor :changed_at
|
42
|
+
end
|
43
|
+
|
44
|
+
class Flower
|
45
|
+
include Volute
|
46
|
+
|
47
|
+
attr_accessor :colour
|
48
|
+
end
|
31
49
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
50
|
+
To make sure that only instance of Light will be concerned, one could write :
|
51
|
+
|
52
|
+
volute Light do
|
53
|
+
volute :colour do
|
54
|
+
object.changed_at = Time.now
|
36
55
|
end
|
56
|
+
end
|
57
|
+
|
58
|
+
Inside of a volute, these are the available 'variables' :
|
59
|
+
|
60
|
+
* object - the instance whose attribute has been set
|
61
|
+
* attribute - the attribute name whose value has been set
|
62
|
+
* previous_value - the previous value for the attribute
|
63
|
+
* value - the new value
|
37
64
|
|
38
|
-
|
65
|
+
thus :
|
39
66
|
|
40
|
-
|
67
|
+
volute Light do
|
68
|
+
volute :colour do
|
69
|
+
puts "#{object.class}.#{attribute} : #{previous_value.inspect} --> #{value.inspect}"
|
41
70
|
end
|
42
71
|
end
|
43
72
|
|
44
|
-
|
45
|
-
|
73
|
+
l = Light.new
|
74
|
+
l.colour = :blue
|
75
|
+
l.colour = :red
|
76
|
+
|
77
|
+
would output :
|
78
|
+
|
79
|
+
Light.colour : nil --> :blue
|
80
|
+
Light.colour : :blue --> :red
|
81
|
+
|
82
|
+
|
83
|
+
== filters / guards
|
84
|
+
|
85
|
+
A volute combines a list of arguments with a block of ruby code
|
86
|
+
|
87
|
+
volute do
|
88
|
+
puts 'some attribute was set'
|
89
|
+
end
|
90
|
+
|
91
|
+
volute Light do
|
92
|
+
puts 'some attribute of an instance of class Light was set'
|
93
|
+
end
|
94
|
+
|
95
|
+
volute Light, Flower do
|
96
|
+
puts 'some attribute of an instance of class Light or Flower was set'
|
97
|
+
end
|
98
|
+
|
99
|
+
volute :count do
|
100
|
+
puts 'the attribute :count of some instance got set'
|
101
|
+
end
|
102
|
+
|
103
|
+
volute :count, :number do
|
104
|
+
puts 'the attribute :count or :number of some instance got set'
|
105
|
+
end
|
106
|
+
|
107
|
+
volute Light, :count do
|
108
|
+
puts 'some attribute of an instance of class Light was set'
|
109
|
+
puts 'OR'
|
110
|
+
puts 'the attribute :count of some instance got set'
|
111
|
+
end
|
112
|
+
|
113
|
+
As soon as 1 argument matches, the Ruby block of the volute is executed. In other words, arg0 OR arg1 OR ... OR argN
|
114
|
+
|
115
|
+
If you need for an AND, read on to "nesting volutes".
|
116
|
+
|
117
|
+
Filtering on attributes who match a regular expression :
|
118
|
+
|
119
|
+
class Invoice
|
120
|
+
include Volute
|
121
|
+
|
122
|
+
attr_accessor :amount
|
123
|
+
attr_accessor :customer_name, :customer_id
|
124
|
+
end
|
125
|
+
|
126
|
+
volute /^customer_/ do
|
127
|
+
puts "attribute :customer_name or :customer_id got modified"
|
128
|
+
end
|
129
|
+
|
130
|
+
|
131
|
+
== 'transition volutes'
|
132
|
+
|
133
|
+
It's possible to filter based on the previous value and the new value (with :any as a wildcard) :
|
134
|
+
|
135
|
+
volute 0 => 100 do
|
136
|
+
puts "some attribute went from 0 to 100"
|
137
|
+
end
|
138
|
+
|
139
|
+
volute :any => 100 do
|
140
|
+
puts "some attribute was just set to 100"
|
141
|
+
end
|
142
|
+
|
143
|
+
volute 0 => :any do
|
144
|
+
puts "some attribute was at 0 and just got changed"
|
145
|
+
end
|
146
|
+
|
147
|
+
Multiple start and end values may be specified :
|
148
|
+
|
149
|
+
volute [ 'FRA', 'ZRH' ] => :any do
|
150
|
+
puts "left FRA or ZRH"
|
151
|
+
end
|
152
|
+
|
153
|
+
volute 'GVA' => [ 'SHA', 'NRT' ] do
|
154
|
+
puts "reached SHA or NRT from GVA"
|
155
|
+
end
|
156
|
+
|
157
|
+
Regular expressions are OK :
|
158
|
+
|
159
|
+
volute /^S..$/ => /^F..$/ do
|
160
|
+
puts "left S.. and reached F.."
|
161
|
+
end
|
162
|
+
|
163
|
+
|
164
|
+
== volute :not
|
165
|
+
|
166
|
+
A volute may have :not has a first argument
|
167
|
+
|
168
|
+
volute :not, Invoice do
|
169
|
+
puts "some instance that is not an invoice..."
|
170
|
+
end
|
171
|
+
|
172
|
+
volute :not, :any => :delivered do
|
173
|
+
puts "a transition to something different than :delivered..."
|
174
|
+
end
|
175
|
+
|
176
|
+
volute :not, Invoice, :paid do
|
177
|
+
puts "not an Invoice and not a variation of the :paid attribute..."
|
178
|
+
end
|
179
|
+
|
180
|
+
Not Bob or Charlie, Nor Bob and neither Charlie.
|
181
|
+
|
46
182
|
|
47
|
-
|
183
|
+
== nesting volutes
|
48
184
|
|
49
|
-
|
50
|
-
# is equivalent to
|
51
|
-
# object.instance_variable_set(:@state, x)
|
185
|
+
Whereas enumerating arguments for a single volute played like an OR, to achieve AND, one can nest volutes.
|
52
186
|
|
53
|
-
|
54
|
-
|
187
|
+
volute Invoice do
|
188
|
+
volute :paid do
|
189
|
+
puts "the :paid attribute of an Invoice just changed"
|
55
190
|
end
|
56
|
-
|
57
|
-
|
191
|
+
end
|
192
|
+
|
193
|
+
volute Grant do
|
194
|
+
volute :paid do
|
195
|
+
puts "the :paid attribute of a Grant just changed"
|
58
196
|
end
|
59
|
-
|
60
|
-
|
197
|
+
end
|
198
|
+
|
199
|
+
|
200
|
+
== 'guarding' inside of the volute block
|
201
|
+
|
202
|
+
As long as one doesn't use a 'return' inside of a block, they're just ruby code...
|
203
|
+
|
204
|
+
volute Patient do
|
205
|
+
if object.sore_throat == true && object.fever == true
|
206
|
+
puts "needs further investigation"
|
207
|
+
elsif object.fever == true
|
208
|
+
puts "only a small fever"
|
61
209
|
end
|
62
210
|
end
|
63
211
|
|
64
|
-
#
|
65
|
-
# trying
|
66
212
|
|
67
|
-
|
68
|
-
p e # => 1.0 km, 1.0 h, 1.0 kph
|
213
|
+
== 'state volutes'
|
69
214
|
|
70
|
-
|
71
|
-
p e # => 1.0 km, 0.1 h, 10.0 kph
|
215
|
+
"I want this volute to trigger when the patient has a sore_throat and the flu", would translate to
|
72
216
|
|
73
|
-
|
74
|
-
|
217
|
+
volute Patient do
|
218
|
+
volute :sore_throat do
|
219
|
+
if value == true && object.fever == true
|
220
|
+
puts "it triggers"
|
221
|
+
end
|
222
|
+
end
|
223
|
+
volute :fever do
|
224
|
+
if value == true && object.sore_throat == true
|
225
|
+
puts "it triggers"
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
75
229
|
|
230
|
+
hairy isn't it ? There is a simpler way, it constitutes an exception to the "volute arguments join in an OR", but it reads well (I hope) :
|
76
231
|
|
77
|
-
|
232
|
+
volute Patient do
|
233
|
+
volute :sore_throat => true, :fever => true do
|
234
|
+
puts "it triggers"
|
235
|
+
end
|
236
|
+
end
|
78
237
|
|
79
|
-
|
80
|
-
#
|
81
|
-
# a state-machine-ish example, inspired by the example at
|
82
|
-
# http://github.com/qoobaa/transitions
|
238
|
+
:not applies as well :
|
83
239
|
|
84
|
-
|
240
|
+
volute Patient do
|
241
|
+
volute :not, :leg_broken do
|
242
|
+
send_patient_back_home # we only treat broken legs
|
243
|
+
end
|
244
|
+
end
|
85
245
|
|
86
|
-
|
246
|
+
Our sore_throat and flu example could be rewritten with Ruby ifs as :
|
87
247
|
|
88
|
-
|
89
|
-
|
248
|
+
volute Patient do
|
249
|
+
if object.sore_throat == true && object.flu == true
|
250
|
+
puts "it triggers"
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
which isn't hairy at all.
|
255
|
+
|
256
|
+
Pointing to multiple values is OK :
|
257
|
+
|
258
|
+
volute Package do
|
259
|
+
volute :delivered => true, :weight => [ '1kg', '2kg' ] do
|
260
|
+
puts "delivered a package of 1 or 2 kg"
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
Not mentioning an attribute implies its value doesn't matter when matching state, :any and :not_nil could prove useful though :
|
265
|
+
|
266
|
+
volute Package do
|
267
|
+
volute :delivered => :not_nil do
|
268
|
+
puts "package entered delivery circuit"
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
volute Item do
|
273
|
+
volute :weight => :any, :package => true do
|
274
|
+
# dropping ":weight => :any" would make sense, but sometimes, when
|
275
|
+
# tweaking volutes, a quick editing of :any to another value is
|
276
|
+
# almost effortless
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
For attributes whose values are strings, regular expressions may prove useful :
|
281
|
+
|
282
|
+
volute :location => /^Fort .+/ do
|
283
|
+
puts "Somewhere in Fort ..."
|
284
|
+
end
|
285
|
+
|
286
|
+
There is an example that uses those 'state volutes' at http://github.com/jmettraux/volute/blob/master/examples/diagnosis.rb
|
90
287
|
|
91
|
-
class Book
|
92
|
-
include Volute
|
93
288
|
|
94
|
-
|
95
|
-
attr_accessor :discontinued
|
289
|
+
== 'over'
|
96
290
|
|
97
|
-
|
291
|
+
Each volute that matches sees its block called. In order to prevent further evaluations, the 'over' method can be called.
|
98
292
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
293
|
+
volute Package do
|
294
|
+
|
295
|
+
volute do
|
296
|
+
over if object.delivered
|
297
|
+
# prevent further volute evaluation if the package was delivered
|
298
|
+
end
|
299
|
+
|
300
|
+
volute :location do
|
301
|
+
(object.comment ||= []) << value
|
103
302
|
end
|
104
303
|
end
|
105
304
|
|
106
|
-
#
|
107
|
-
# a volute triggered for any 'set' operation on an attribute of book
|
108
305
|
|
109
|
-
|
306
|
+
== application of the volutes on demand
|
110
307
|
|
111
|
-
|
112
|
-
# is equivalent to
|
113
|
-
# object.instance_variable_set(:@state, x)
|
308
|
+
Up until now, this readme focused on the scenario where volute application is triggered by a change in the state of an attribute (in a class that includes Volute).
|
114
309
|
|
115
|
-
|
116
|
-
|
117
|
-
|
310
|
+
It is entirely OK to have classes that do not include Volute but are the object of a volute application :
|
311
|
+
|
312
|
+
class Engine
|
313
|
+
attr_accessor :state
|
314
|
+
def turn_key!
|
315
|
+
@key_turned = true
|
316
|
+
Volute.apply(self, :key_turned)
|
317
|
+
end
|
318
|
+
def press_red_button!
|
319
|
+
Volute.apply(self)
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
volute Engine do
|
324
|
+
if attribute == :key_turned
|
325
|
+
object.state = :running
|
118
326
|
else
|
119
|
-
object.
|
120
|
-
:state, :in_stock)
|
327
|
+
object.state = :off
|
121
328
|
end
|
122
329
|
end
|
123
330
|
|
124
|
-
|
125
|
-
|
331
|
+
The key here is the call to
|
332
|
+
|
333
|
+
Volute.apply(object, attribute=nil, previous_value=nil, value=nil)
|
334
|
+
|
335
|
+
In fact, for classes that include Volute, this method is called for each attribute getting set.
|
336
|
+
|
337
|
+
This technique is also a key when building system where the volutes aren't called all the time but only right before their result should matter ('decision' versus 'reaction').
|
338
|
+
|
339
|
+
|
340
|
+
== volute blocks, closures
|
341
|
+
|
342
|
+
TODO
|
343
|
+
|
344
|
+
|
345
|
+
== volute management
|
346
|
+
|
347
|
+
TODO
|
348
|
+
|
349
|
+
|
350
|
+
== examples
|
351
|
+
|
352
|
+
http://github.com/jmettraux/volute/tree/master/examples/
|
353
|
+
|
354
|
+
|
355
|
+
== alternatives
|
356
|
+
|
357
|
+
states
|
358
|
+
|
359
|
+
- there is a list of ruby state machines at the end of http://jmettraux.wordpress.com/2009/07/03/state-machine-workflow-engine/
|
360
|
+
|
361
|
+
rules
|
362
|
+
|
363
|
+
- http://github.com/codeaspects/ruleby
|
364
|
+
- http://rools.rubyforge.org/
|
365
|
+
- ...
|
366
|
+
|
367
|
+
aspects
|
368
|
+
|
369
|
+
- http://github.com/gcao/aspect4r
|
370
|
+
- http://github.com/teejayvanslyke/gazer
|
371
|
+
- http://github.com/nakajima/aspectory
|
372
|
+
- http://github.com/matthewrudy/aspicious
|
373
|
+
- http://aquarium.rubyforge.org/
|
374
|
+
|
375
|
+
- http://github.com/search?type=Repositories&language=ruby&q=aspect&repo=&langOverride=&x=15&y=25&start_value=1 (github search)
|
376
|
+
|
377
|
+
hooks and callbacks
|
378
|
+
|
379
|
+
- http://github.com/apotonick/hooks
|
380
|
+
- http://github.com/avdi/hookr
|
381
|
+
- http://github.com/auser/backcall
|
382
|
+
- ...
|
383
|
+
|
384
|
+
|
385
|
+
== author
|
126
386
|
|
127
|
-
|
387
|
+
John Mettraux - http://github.com/jmettraux/
|
128
388
|
|
129
|
-
emma.stock = 2
|
130
|
-
p emma.state # => :in_stock
|
131
389
|
|
132
|
-
|
133
|
-
p emma.state # => :out_of_stock
|
390
|
+
== feedback
|
134
391
|
|
135
|
-
|
136
|
-
|
392
|
+
* IRC freenode #ruote
|
393
|
+
* jmettraux@gmail.com
|
137
394
|
|
138
395
|
|
139
396
|
== license
|