volute 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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/lib/volute.rb
CHANGED
@@ -22,9 +22,12 @@
|
|
22
22
|
# Made in Japan.
|
23
23
|
#++
|
24
24
|
|
25
|
+
|
26
|
+
# TODO : insert lengthy explanation here
|
27
|
+
#
|
25
28
|
module Volute
|
26
29
|
|
27
|
-
VOLUTE_VERSION = '0.1.
|
30
|
+
VOLUTE_VERSION = '0.1.1'
|
28
31
|
|
29
32
|
#
|
30
33
|
# adding class methods to target classes
|
@@ -65,7 +68,7 @@ module Volute
|
|
65
68
|
|
66
69
|
previous_value = volute_get(key)
|
67
70
|
vset(key, value)
|
68
|
-
Volute.
|
71
|
+
Volute.apply(self, key, previous_value, value)
|
69
72
|
|
70
73
|
value
|
71
74
|
end
|
@@ -73,28 +76,94 @@ module Volute
|
|
73
76
|
#
|
74
77
|
# Volute class methods
|
75
78
|
|
76
|
-
def self
|
79
|
+
def self.register(args, block)
|
77
80
|
|
78
|
-
|
81
|
+
top << [ args, block ]
|
79
82
|
end
|
80
83
|
|
81
84
|
# Nukes all the top level volutes.
|
82
85
|
#
|
83
86
|
def self.clear!
|
84
87
|
|
85
|
-
|
88
|
+
@top = nil
|
86
89
|
end
|
87
90
|
|
88
|
-
|
91
|
+
# Volute.apply is generally called from the setter of a class which include
|
92
|
+
# Volute, but it's OK to call it directly, to force volute application.
|
93
|
+
#
|
94
|
+
# class Engine
|
95
|
+
# attr_accessor :state
|
96
|
+
# def turn_key!
|
97
|
+
# @key_turned = true
|
98
|
+
# Volute.apply(self, :key_turned)
|
99
|
+
# end
|
100
|
+
# def press_red_button!
|
101
|
+
# Volute.apply(self)
|
102
|
+
# end
|
103
|
+
# end
|
104
|
+
#
|
105
|
+
# volute Engine do
|
106
|
+
# if attribute == :key_turned
|
107
|
+
# object.state = :running
|
108
|
+
# else
|
109
|
+
# object.state = :off
|
110
|
+
# end
|
111
|
+
# end
|
112
|
+
#
|
113
|
+
def self.apply(object, attribute=nil, previous_value=nil, value=nil)
|
89
114
|
|
90
115
|
target = Target.new(object, attribute, previous_value, value)
|
91
116
|
|
92
|
-
|
117
|
+
top.each { |args, block| target.volute(*args, &block) }
|
118
|
+
end
|
119
|
+
|
120
|
+
def self.top
|
121
|
+
|
122
|
+
(@top ||= VoluteArray.new)
|
93
123
|
end
|
94
124
|
|
95
125
|
#
|
96
126
|
# some classes
|
97
127
|
|
128
|
+
# Subclassing Array to add a #filter and a #remove method, so that
|
129
|
+
# things like
|
130
|
+
#
|
131
|
+
# all_the_volutes = volutes
|
132
|
+
# volutes_about_class_x = volutes(x)
|
133
|
+
#
|
134
|
+
# volutes.remove(x)
|
135
|
+
# # just removed all the volutes referring class or attribute x
|
136
|
+
#
|
137
|
+
class VoluteArray < Array
|
138
|
+
|
139
|
+
def filter(arg)
|
140
|
+
|
141
|
+
select { |args, block|
|
142
|
+
|
143
|
+
classes = args.select { |a| a.is_a?(Class) }
|
144
|
+
|
145
|
+
if args.include?(arg)
|
146
|
+
true
|
147
|
+
elsif arg.is_a?(Class)
|
148
|
+
(arg.ancestors & classes).size > 0
|
149
|
+
elsif arg.is_a?(Module)
|
150
|
+
(arg.constants.collect { |c| arg.const_get(c) } & classes).size > 0
|
151
|
+
#elsif arg.is_a?(Symbol)
|
152
|
+
# already handled by the initial if
|
153
|
+
else
|
154
|
+
false
|
155
|
+
end
|
156
|
+
}
|
157
|
+
end
|
158
|
+
|
159
|
+
def remove(arg)
|
160
|
+
|
161
|
+
filtered = filter(arg)
|
162
|
+
|
163
|
+
reject! { |volute| filtered.include?(volute) }
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
98
167
|
class Target
|
99
168
|
|
100
169
|
attr_reader :object, :attribute, :previous_value, :value
|
@@ -112,7 +181,9 @@ module Volute
|
|
112
181
|
def volute(*args, &block)
|
113
182
|
|
114
183
|
return if @over
|
115
|
-
|
184
|
+
|
185
|
+
match = (args.first == :not) ? ( ! match?(args[1..-1])) : match?(args)
|
186
|
+
return unless match
|
116
187
|
|
117
188
|
self.instance_eval(&block)
|
118
189
|
end
|
@@ -122,50 +193,106 @@ module Volute
|
|
122
193
|
@over = true
|
123
194
|
end
|
124
195
|
|
125
|
-
#def is(val)
|
126
|
-
# val == value
|
127
|
-
#end
|
128
|
-
#def was(val)
|
129
|
-
# val == previous_value
|
130
|
-
#end
|
131
|
-
|
132
196
|
protected
|
133
197
|
|
134
|
-
def match?
|
198
|
+
def match?(args)
|
199
|
+
|
200
|
+
return true if args.empty?
|
201
|
+
|
202
|
+
return state_match?(args) if is_a_state_match?(args)
|
135
203
|
|
136
204
|
classes = args.select { |a| a.is_a?(Class) }
|
137
205
|
args.select { |a| a.is_a?(Module) }.each { |m|
|
138
206
|
classes.concat(m.constants.collect { |c| m.const_get(c) })
|
139
207
|
}
|
140
|
-
|
141
|
-
return false
|
142
|
-
end
|
208
|
+
return true if (classes & @object.class.ancestors).size > 0
|
143
209
|
|
144
210
|
atts = args.select { |a| a.is_a?(Symbol) }
|
145
|
-
|
146
|
-
|
211
|
+
return true if atts.include?(attribute.to_sym)
|
212
|
+
|
213
|
+
atts = args.select { |a| a.is_a?(Regexp) }
|
214
|
+
return true if atts.find { |r| r.match(attribute.to_s) }
|
215
|
+
|
216
|
+
return transition_match?(args.last) if args.last.is_a?(Hash)
|
217
|
+
|
218
|
+
false
|
219
|
+
end
|
220
|
+
|
221
|
+
def has_attribute?(att)
|
222
|
+
|
223
|
+
return false if att == :any
|
224
|
+
|
225
|
+
#m = @object.method(att) rescue nil
|
226
|
+
#return false unless m
|
227
|
+
#return false if m.arity != -1
|
228
|
+
# arity would be -1 on ruby 1.8.7 and 0 on ruby 1.9.1, ...
|
229
|
+
|
230
|
+
begin
|
231
|
+
@object.send(att)
|
232
|
+
true
|
233
|
+
rescue NoMethodError => nme
|
234
|
+
false
|
147
235
|
end
|
236
|
+
end
|
148
237
|
|
149
|
-
|
238
|
+
def is_a_state_match?(args)
|
150
239
|
|
151
|
-
|
240
|
+
state = args.first
|
241
|
+
|
242
|
+
return false if args.length != 1
|
243
|
+
return false unless state.is_a?(Hash)
|
244
|
+
return false if state.keys.find { |arg| ! arg.is_a?(Symbol) }
|
245
|
+
return false if state.keys.find { |att| ! has_attribute?(att) }
|
246
|
+
true
|
247
|
+
end
|
152
248
|
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
249
|
+
def val_match?(target, current, in_array=false)
|
250
|
+
|
251
|
+
return true if target == current
|
252
|
+
return true if target == :any
|
253
|
+
return current != nil if target == :not_nil
|
254
|
+
|
255
|
+
if target.is_a?(Regexp) && current.is_a?(String)
|
256
|
+
return target.match(current)
|
257
|
+
end
|
258
|
+
if in_array == false && target.is_a?(Array)
|
259
|
+
return target.find { |t| val_match?(t, current, true) }
|
158
260
|
end
|
159
261
|
|
160
|
-
|
161
|
-
|
162
|
-
|
262
|
+
false
|
263
|
+
end
|
264
|
+
|
265
|
+
def state_match?(args)
|
266
|
+
|
267
|
+
args.first.each do |att, target_val|
|
268
|
+
return false unless val_match?(target_val, @object.send(att))
|
269
|
+
end
|
270
|
+
true
|
271
|
+
end
|
272
|
+
|
273
|
+
def transition_match?(hash)
|
274
|
+
|
275
|
+
hash.each do |startv, endv|
|
276
|
+
if val_match?(startv, previous_value) && val_match?(endv, value)
|
277
|
+
return true
|
278
|
+
end
|
279
|
+
end
|
280
|
+
false
|
163
281
|
end
|
164
282
|
end
|
165
283
|
end
|
166
284
|
|
285
|
+
# Registers a 'volute' at the top level (ie a volute not nested into another)
|
286
|
+
#
|
167
287
|
def volute(*args, &block)
|
168
288
|
|
169
|
-
Volute
|
289
|
+
Volute.register(args, block)
|
290
|
+
end
|
291
|
+
|
292
|
+
# With no arguments, it will list all the top-level volutes.
|
293
|
+
#
|
294
|
+
def volutes(arg=nil)
|
295
|
+
|
296
|
+
arg ? Volute.top.filter(arg) : Volute.top
|
170
297
|
end
|
171
298
|
|
data/spec/apply_spec.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
|
2
|
+
require File.join(File.dirname(__FILE__), 'spec_helper.rb')
|
3
|
+
|
4
|
+
|
5
|
+
class Engine
|
6
|
+
attr_accessor :state
|
7
|
+
|
8
|
+
def turn_key!
|
9
|
+
@key_turned = true
|
10
|
+
Volute.apply(self, :key_turned)
|
11
|
+
end
|
12
|
+
|
13
|
+
def press_red_button!
|
14
|
+
Volute.apply(self)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
describe 'Volute.apply' do
|
20
|
+
|
21
|
+
before(:each) do
|
22
|
+
|
23
|
+
Volute.clear!
|
24
|
+
|
25
|
+
volute Engine do
|
26
|
+
if attribute == :key_turned
|
27
|
+
object.state = :running
|
28
|
+
else
|
29
|
+
object.state = :off
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
@engine = Engine.new
|
34
|
+
|
35
|
+
volute Package do
|
36
|
+
volute do
|
37
|
+
over if object.delivered
|
38
|
+
end
|
39
|
+
volute :location do
|
40
|
+
(object.comment ||= []) << value
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'should apply volutes' do
|
46
|
+
|
47
|
+
@engine.turn_key!
|
48
|
+
|
49
|
+
@engine.state.should == :running
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
@@ -0,0 +1,58 @@
|
|
1
|
+
|
2
|
+
require File.join(File.dirname(__FILE__), 'spec_helper.rb')
|
3
|
+
|
4
|
+
|
5
|
+
describe 'a volute with a :not as first arg' do
|
6
|
+
|
7
|
+
before(:each) do
|
8
|
+
|
9
|
+
@item = Item.new
|
10
|
+
@invoice = Invoice.new
|
11
|
+
end
|
12
|
+
|
13
|
+
describe 'and classes as further args' do
|
14
|
+
|
15
|
+
before(:each) do
|
16
|
+
|
17
|
+
Volute.clear!
|
18
|
+
|
19
|
+
volute :not, Item do
|
20
|
+
object.comment = 'not an item'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'should not affect the negated class' do
|
25
|
+
|
26
|
+
@item.weight = '12 pounds'
|
27
|
+
|
28
|
+
@item.comment.should == nil
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'should affect other classes' do
|
32
|
+
|
33
|
+
@invoice.paid = true
|
34
|
+
|
35
|
+
@invoice.comment.should == 'not an item'
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe 'and nothing else' do
|
40
|
+
|
41
|
+
before(:each) do
|
42
|
+
|
43
|
+
Volute.clear!
|
44
|
+
|
45
|
+
volute :not do
|
46
|
+
object.comment = 'NEVER !'
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'should never trigger' do
|
51
|
+
|
52
|
+
@item.weight = '1kg'
|
53
|
+
|
54
|
+
@item.comment.should == nil
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
@@ -126,64 +126,92 @@ describe 'a volute for an attribute' do
|
|
126
126
|
end
|
127
127
|
end
|
128
128
|
|
129
|
-
describe '
|
129
|
+
describe 'a volute for a class or an attribute' do
|
130
130
|
|
131
131
|
before(:each) do
|
132
132
|
|
133
133
|
Volute.clear!
|
134
134
|
|
135
|
+
@item = Item.new
|
135
136
|
@package = Package.new
|
136
|
-
@package.vset(:location, 'NRT')
|
137
137
|
|
138
|
-
volute
|
139
|
-
object.comment = '
|
140
|
-
end
|
141
|
-
volute :location, 'NRT' => 'SFO' do
|
142
|
-
object.comment = 'reached SFO from NRT'
|
143
|
-
end
|
144
|
-
volute 'NRT' => 'FCO' do
|
145
|
-
object.comment = 'reached FCO from NRT'
|
146
|
-
end
|
147
|
-
volute 'GVA' => :any do
|
148
|
-
object.comment = 'left GVA'
|
138
|
+
volute Item, :delivered do
|
139
|
+
object.comment = 'Item, :delivered'
|
149
140
|
end
|
150
141
|
end
|
151
142
|
|
152
|
-
it 'should not trigger
|
143
|
+
it 'should not trigger inappropriately' do
|
153
144
|
|
154
|
-
@package.location = '
|
145
|
+
@package.location = 'Baden Baden'
|
155
146
|
|
156
147
|
@package.comment.should == nil
|
157
148
|
end
|
158
149
|
|
159
|
-
it 'should trigger for
|
150
|
+
it 'should trigger for the class' do
|
151
|
+
|
152
|
+
@item.weight = :heavy
|
160
153
|
|
161
|
-
@
|
162
|
-
|
154
|
+
@item.comment.should == 'Item, :delivered'
|
155
|
+
end
|
156
|
+
|
157
|
+
it 'should trigger for the attribute' do
|
158
|
+
|
159
|
+
@package.delivered = true
|
163
160
|
|
164
|
-
@package.comment.should == '
|
161
|
+
@package.comment.should == 'Item, :delivered'
|
165
162
|
end
|
163
|
+
end
|
166
164
|
|
167
|
-
|
165
|
+
describe 'a volute for a class' do
|
168
166
|
|
169
|
-
|
167
|
+
before(:each) do
|
168
|
+
|
169
|
+
Volute.clear!
|
170
170
|
|
171
|
-
@
|
171
|
+
@item = Item.new
|
172
|
+
@heavy_item = HeavyItem.new
|
173
|
+
|
174
|
+
volute Item do
|
175
|
+
(object.comment ||= []) << 'regular'
|
176
|
+
end
|
177
|
+
volute HeavyItem do
|
178
|
+
(object.comment ||= []) << 'heavy'
|
179
|
+
end
|
172
180
|
end
|
173
181
|
|
174
|
-
it 'should trigger for a
|
182
|
+
it 'should trigger for a child class' do
|
175
183
|
|
176
|
-
@
|
184
|
+
@heavy_item.delivered = true
|
177
185
|
|
178
|
-
@
|
186
|
+
@heavy_item.comment.should == [ 'regular', 'heavy' ]
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
describe 'a volute with a regex arg' do
|
191
|
+
|
192
|
+
before(:each) do
|
193
|
+
|
194
|
+
Volute.clear!
|
195
|
+
|
196
|
+
@invoice = Invoice.new
|
197
|
+
|
198
|
+
volute /^customer_/ do
|
199
|
+
object.comment = 'set on customer_ attribute called'
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
it 'should not trigger for attributes whose name doesn\'t match' do
|
204
|
+
|
205
|
+
@invoice.paid = true
|
206
|
+
|
207
|
+
@invoice.comment.should == nil
|
179
208
|
end
|
180
209
|
|
181
|
-
it 'should trigger for
|
210
|
+
it 'should trigger for attributes whose name matches' do
|
182
211
|
|
183
|
-
@
|
184
|
-
@package.location = 'CAL'
|
212
|
+
@invoice.customer_name = 'tojo'
|
185
213
|
|
186
|
-
@
|
214
|
+
@invoice.comment.should == 'set on customer_ attribute called'
|
187
215
|
end
|
188
216
|
end
|
189
217
|
|