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/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
|
|