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.
@@ -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