volute 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.0'
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.root_eval(self, key, previous_value, value)
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.<<(block)
79
+ def self.register(args, block)
77
80
 
78
- (@top ||= []) << block
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
- (@top = [])
88
+ @top = nil
86
89
  end
87
90
 
88
- def self.root_eval(object, attribute, previous_value, value)
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
- (@top || []).each { |args, block| target.volute(*args, &block) }
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
- return unless match?(args)
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? (args)
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
- if classes.size > 0 && (classes & object.class.ancestors).empty?
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
- if atts.size > 0 && ( ! atts.include?(attribute.to_sym))
146
- return false
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
- opts = args.last.is_a?(Hash) ? args.pop : {}
238
+ def is_a_state_match?(args)
150
239
 
151
- return true if opts.empty?
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
- opts.inject(false) do |b, (k, v)|
154
- b || (
155
- (k == :any || k == previous_value) &&
156
- (v == :any || v == value)
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
- #rescue Exception => e
161
- # p e
162
- # e.backtrace.each { |l| p l }
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 << [ args, block ]
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
 
@@ -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 'transition volutes' do
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 :location, :any => 'SFO' do
139
- object.comment = 'reached SFO'
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 when not specified' do
143
+ it 'should not trigger inappropriately' do
153
144
 
154
- @package.location = 'ZRH'
145
+ @package.location = 'Baden Baden'
155
146
 
156
147
  @package.comment.should == nil
157
148
  end
158
149
 
159
- it 'should trigger for an end state' do
150
+ it 'should trigger for the class' do
151
+
152
+ @item.weight = :heavy
160
153
 
161
- @package.vset(:location, 'ZRH')
162
- @package.location = 'SFO'
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 == 'reached SFO'
161
+ @package.comment.should == 'Item, :delivered'
165
162
  end
163
+ end
166
164
 
167
- it 'should trigger for an attribute, a start state and an end state' do
165
+ describe 'a volute for a class' do
168
166
 
169
- @package.location = 'SFO'
167
+ before(:each) do
168
+
169
+ Volute.clear!
170
170
 
171
- @package.comment.should == 'reached SFO from NRT'
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 start state and an end state' do
182
+ it 'should trigger for a child class' do
175
183
 
176
- @package.location = 'FCO'
184
+ @heavy_item.delivered = true
177
185
 
178
- @package.comment.should == 'reached FCO from NRT'
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 a start state' do
210
+ it 'should trigger for attributes whose name matches' do
182
211
 
183
- @package.vset(:location, 'GVA')
184
- @package.location = 'CAL'
212
+ @invoice.customer_name = 'tojo'
185
213
 
186
- @package.comment.should == 'left GVA'
214
+ @invoice.comment.should == 'set on customer_ attribute called'
187
215
  end
188
216
  end
189
217