volute 0.1.0 → 0.1.1

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