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.
@@ -2,5 +2,23 @@
2
2
  = volute - CHANGELOG.txt
3
3
 
4
4
 
5
- == volute - 0.1.0 not yet released
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
 
@@ -1,139 +1,396 @@
1
1
 
2
2
  = volute
3
3
 
4
- It could be a 'set event bus', or a 'business logic relocator'.
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
- == usage
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
- gem install volute
13
+ require 'rubygems'
14
+ require 'volute' # gem install volute
14
15
 
16
+ class Light
17
+ include Volute
15
18
 
16
- == example : equation
19
+ attr_accessor :colour
20
+ attr_accessor :changed_at
21
+ end
17
22
 
18
- # license is MIT
23
+ volute :colour do
24
+ object.changed_at = Time.now
25
+ end
19
26
 
20
- $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
27
+ l = Light.new
28
+ p l # => #<Light:0x10014c480>
21
29
 
22
- require 'volute'
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
- class Equation
35
+ Those two classes would see their :colour hooked :
36
+
37
+ class Light
28
38
  include Volute
29
39
 
30
- attr_accessor :km, :h, :kph
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
- def initialize
33
- @km = 1.0
34
- @h = 1.0
35
- @kph = 1.0
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
- def inspect
65
+ thus :
39
66
 
40
- "#{@km} km, #{@h} h, #{@kph} kph"
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
- # a volute triggered for any 'set' operation on an attribute of book
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
- volute Equation do
183
+ == nesting volutes
48
184
 
49
- # object.vset(:state, x)
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
- volute :km do
54
- object.vset(:h, value / object.kph)
187
+ volute Invoice do
188
+ volute :paid do
189
+ puts "the :paid attribute of an Invoice just changed"
55
190
  end
56
- volute :h do
57
- object.vset(:kph, object.km / value)
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
- volute :kph do
60
- object.vset(:h, object.km / value)
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
- e = Equation.new
68
- p e # => 1.0 km, 1.0 h, 1.0 kph
213
+ == 'state volutes'
69
214
 
70
- e.kph = 10.0
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
- e.km = 5.0
74
- p e # => 5.0 km, 0.5 h, 10.0 kph
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
- == example : some kind of state-machine
232
+ volute Patient do
233
+ volute :sore_throat => true, :fever => true do
234
+ puts "it triggers"
235
+ end
236
+ end
78
237
 
79
- # license is MIT
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
- $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
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
- require 'volute'
246
+ Our sore_throat and flu example could be rewritten with Ruby ifs as :
87
247
 
88
- #
89
- # our class
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
- attr_accessor :stock
95
- attr_accessor :discontinued
289
+ == 'over'
96
290
 
97
- attr_reader :state
291
+ Each volute that matches sees its block called. In order to prevent further evaluations, the 'over' method can be called.
98
292
 
99
- def initialize (stock)
100
- @stock = stock
101
- @discontinued = false
102
- @state = :in_stock
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
- volute Book do
306
+ == application of the volutes on demand
110
307
 
111
- # object.volute_do_set(:state, x)
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
- if object.stock <= 0
116
- object.volute_do_set(
117
- :state, object.discontinued ? :discontinued : :out_of_stock)
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.volute_do_set(
120
- :state, :in_stock)
327
+ object.state = :off
121
328
  end
122
329
  end
123
330
 
124
- #
125
- # trying
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
- emma = Book.new(10)
387
+ John Mettraux - http://github.com/jmettraux/
128
388
 
129
- emma.stock = 2
130
- p emma.state # => :in_stock
131
389
 
132
- emma.stock = 0
133
- p emma.state # => :out_of_stock
390
+ == feedback
134
391
 
135
- emma.discontinued = true
136
- p emma.state # => :discontinued
392
+ * IRC freenode #ruote
393
+ * jmettraux@gmail.com
137
394
 
138
395
 
139
396
  == license