davidlee-state-fu 0.2.0 → 0.3.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.
Files changed (43) hide show
  1. data/README.textile +6 -2
  2. data/Rakefile +24 -3
  3. data/lib/no_stdout.rb +28 -5
  4. data/lib/state-fu.rb +25 -21
  5. data/lib/state_fu/active_support_lite/misc.rb +57 -0
  6. data/lib/state_fu/binding.rb +51 -41
  7. data/lib/state_fu/core_ext.rb +5 -4
  8. data/lib/state_fu/event.rb +51 -16
  9. data/lib/state_fu/exceptions.rb +5 -0
  10. data/lib/state_fu/fu_space.rb +5 -4
  11. data/lib/state_fu/helper.rb +25 -3
  12. data/lib/state_fu/hooks.rb +4 -1
  13. data/lib/state_fu/interface.rb +20 -24
  14. data/lib/state_fu/lathe.rb +38 -2
  15. data/lib/state_fu/logger.rb +84 -6
  16. data/lib/state_fu/machine.rb +3 -0
  17. data/lib/state_fu/method_factory.rb +3 -3
  18. data/lib/state_fu/persistence/active_record.rb +3 -1
  19. data/lib/state_fu/persistence/attribute.rb +4 -4
  20. data/lib/state_fu/persistence/base.rb +3 -3
  21. data/lib/state_fu/persistence/relaxdb.rb +23 -0
  22. data/lib/state_fu/persistence.rb +24 -29
  23. data/lib/state_fu/plotter.rb +63 -0
  24. data/lib/state_fu/sprocket.rb +12 -0
  25. data/lib/state_fu/state.rb +22 -0
  26. data/lib/state_fu/transition.rb +13 -0
  27. data/lib/vizier.rb +300 -0
  28. data/spec/BDD/plotter_spec.rb +115 -0
  29. data/spec/features/binding_and_transition_helper_mixin_spec.rb +111 -0
  30. data/spec/features/not_requirements_spec.rb +81 -0
  31. data/spec/features/state_and_array_options_accessor_spec.rb +47 -0
  32. data/spec/features/transition_boolean_comparison.rb +90 -0
  33. data/spec/helper.rb +33 -0
  34. data/spec/integration/active_record_persistence_spec.rb +0 -1
  35. data/spec/integration/example_01_document_spec.rb +1 -1
  36. data/spec/integration/relaxdb_persistence_spec.rb +94 -0
  37. data/spec/integration/requirement_reflection_spec.rb +2 -2
  38. data/spec/integration/transition_spec.rb +9 -1
  39. data/spec/units/binding_spec.rb +46 -17
  40. data/spec/units/lathe_spec.rb +11 -10
  41. data/spec/units/method_factory_spec.rb +6 -1
  42. metadata +37 -23
  43. data/spec/integration/temp_spec.rb +0 -17
@@ -211,5 +211,18 @@ module StateFu
211
211
  alias_method :pretend?, :testing?
212
212
  alias_method :dry_run?, :testing?
213
213
 
214
+ # an accepted transition == true
215
+ # an unaccepted transition == false
216
+ # same for === (for case equality)
217
+ def == other
218
+ case other
219
+ when true
220
+ accepted?
221
+ when false
222
+ !accepted?
223
+ else
224
+ super( other )
225
+ end
226
+ end
214
227
  end
215
228
  end
data/lib/vizier.rb ADDED
@@ -0,0 +1,300 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ begin
4
+ require File.join(File.dirname(__FILE__), '/state_fu/core_ext' )
5
+ rescue LoadError
6
+ require 'activesupport'
7
+ end
8
+
9
+ # sorry, there's only Heisendocumentation (if I realize anyone's looking for
10
+ # them, I might write some)
11
+
12
+ # temporary dirty hack
13
+
14
+ module Vizier
15
+
16
+ module Support
17
+ LEGAL_CHARS = 'a-zA-Z0-9_'
18
+
19
+ def attributes=( attrs )
20
+ @attributes = attrs.symbolize_keys!.extend( Attributes )
21
+ end
22
+
23
+ def attributes
24
+ (@attributes ||= {}).extend( Attributes )
25
+ end
26
+
27
+ def legal?( str )
28
+ str =~ /^[#{LEGAL_CHARS}]+$/ && str == str.split
29
+ end
30
+
31
+ def sanitize(str)
32
+ sanitize( str )
33
+ end
34
+
35
+ def quote( str )
36
+ return str if legal?( str )
37
+ '"' + str.to_s.gsub(/"/,'\"') + '"'
38
+ end
39
+
40
+ def self.included( klass )
41
+ klass.extend( ClassMethods )
42
+ end
43
+
44
+ module Finder
45
+ def []( idx )
46
+ begin
47
+ super( idx )
48
+ rescue TypeError => e
49
+ if idx.is_a?( String ) || idx.is_a?( Symbol )
50
+ self.detect { |i| i.name.to_s == idx.to_s }
51
+ elsif idx.class.respond_to?(:table_name)
52
+ self.detect { |i| i.name.to_s == Vizier::Node.make_name( idx ) }
53
+ else
54
+ raise e
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ module ClassMethods
61
+ def sanitize( str )
62
+ str.to_s.gsub(/[^#{LEGAL_CHARS}]/,'_').gsub(/__+/,'_')
63
+ end
64
+
65
+ def finder( name )
66
+ class_eval do
67
+ define_method name do
68
+ instance_variable_get( "@#{name}" ).extend( Finder )
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+
75
+ module Attributes
76
+ include Support
77
+
78
+ def to_s
79
+ return '[]' if empty?
80
+ '[ ' + self.map do |k,v|
81
+ "#{quote k} = #{quote v}"
82
+ end.join(" ") + ' ]'
83
+ end
84
+
85
+ end
86
+
87
+ class Base
88
+ def [](k)
89
+ attributes[k.to_sym]
90
+ end
91
+
92
+ def []=(k,v)
93
+ attributes[k.to_sym] = v
94
+ end
95
+ end
96
+
97
+ class Link < Base
98
+ include Support
99
+ attr_accessor :from
100
+ attr_accessor :to
101
+
102
+ def initialize( from, to, attrs={} )
103
+ self.attributes = attrs
104
+ @from = extract_name( from )
105
+ @to = extract_name( to )
106
+ end
107
+
108
+ def extract_name( o )
109
+ o.is_a?(String) ? o : o.name
110
+ end
111
+
112
+ def to_str
113
+ "#{quote from} -> #{quote to} #{attributes};"
114
+ end
115
+ end
116
+
117
+ # TODO ..
118
+ module Label
119
+ def []( i )
120
+
121
+ end
122
+ end
123
+
124
+ class Node < Base
125
+ include Support
126
+
127
+ attr_accessor :object
128
+ attr_accessor :fields
129
+ attr_accessor :name
130
+
131
+ def initialize( name = nil, attrs={} )
132
+ self.attributes = attrs
133
+ if name.is_a?( String )
134
+ self.name = name
135
+ @label = attrs.delete(:label) || name
136
+ else
137
+ @object = name
138
+ self.name = Node.make_name( @object )
139
+ @label = attrs.delete(:label) || Node.first_response( @object, :name, :identifier, :label ) || name
140
+ end
141
+ end
142
+
143
+ def self.make_name( obj )
144
+ sanitize [ obj.class, first_response( obj, :name, :identifier, :id, :hash)].join('_')
145
+ end
146
+
147
+ def self.first_response obj, *method_names
148
+ responder = method_names.flatten.detect { |m| obj.respond_to?(m) }
149
+ obj.send( responder ) unless responder.nil?
150
+ end
151
+
152
+ def name=( str )
153
+ @name = str.to_s.gsub(/[^a-zA-Z0-9_]/,'_').gsub(/__+/,'_')
154
+ end
155
+
156
+ def to_str
157
+ "#{quote name} #{attributes.to_s};"
158
+ end
159
+
160
+ def to_s
161
+ quote( name )
162
+ end
163
+ end
164
+
165
+ class SubGraph < Base
166
+ include Support
167
+
168
+ finder :nodes
169
+
170
+ attr_accessor :links
171
+ attr_accessor :name
172
+
173
+ def initialize( name, attrs={} )
174
+ self.attributes = attrs
175
+ @node = {}
176
+ @edge = {}
177
+
178
+ @name = name
179
+ @nodes = []
180
+ @links = []
181
+ end
182
+
183
+ def node(attrs={})
184
+ (@node ||= {}).merge!(attrs).extend(Attributes)
185
+ end
186
+
187
+ def graph(attrs={})
188
+ self.attributes.merge!(attrs).extend(Attributes)
189
+ end
190
+
191
+ def edge(attrs={})
192
+ (@edge ||= {}).merge!(attrs).extend(Attributes)
193
+ end
194
+
195
+ def add_node( n, a={} )
196
+ returning Node.new(n,a) do |n|
197
+ @nodes << n
198
+ end
199
+ end
200
+
201
+ def add_link(from, to, a={})
202
+ returning Link.new( from, to, a) do |l|
203
+ @links << l
204
+ end
205
+ end
206
+ alias_method :connect, :add_link
207
+ alias_method :add_edge, :add_link
208
+
209
+ def build(lines = [], indent = 0)
210
+ lines.map do |line|
211
+ if line.is_a?( Array )
212
+ build( line, indent + 1)
213
+ else
214
+ (" " * (indent * 4) ) + line.to_s
215
+ end
216
+ end.join("\n")
217
+ end
218
+
219
+ def write_comment( str, j = 0 )
220
+ l = 40 - (j * 4)
221
+ i = ' ' * (j * 4)
222
+ "\n#{i}/*#{'*'*(l-2)}\n#{i}** #{ str.ljust((l - (6) - (j*4)),' ') }#{i} **\n#{i}#{'*'*(l-1)}/"
223
+ end
224
+
225
+ def comment(str)
226
+ write_comment(str, 2)
227
+ end
228
+
229
+ def to_str
230
+ build( ["subgraph #{quote name} {",
231
+ [ # attributes.map {|k,v| "#{quote k} = #{quote v};" },
232
+ ["graph #{attributes};",
233
+ "node #{node};",
234
+ "edge #{edge};"
235
+ ],
236
+ nodes.map(&:to_str),
237
+ links.map(&:to_str),
238
+ "}"
239
+ ],
240
+ ])
241
+ end
242
+ alias_method :generate!, :to_str
243
+
244
+ end
245
+
246
+ class Graph < SubGraph
247
+ finder :subgraphs
248
+
249
+ def comment( str )
250
+ write_comment( str, 1 )
251
+ end
252
+
253
+ def to_str
254
+ build(["digraph #{quote name} {",
255
+ [
256
+ comment("global options"),
257
+ "graph #{graph};",
258
+ "node #{node};",
259
+ "edge #{edge};"
260
+ ],
261
+ comment("nodes"),
262
+ nodes.map(&:to_str),
263
+ comment("links"),
264
+ links.map(&:to_str),
265
+ comment("subgraphs"),
266
+ subgraphs.map(&:to_str),
267
+ "}"])
268
+ end
269
+ alias_method :generate!, :to_str
270
+
271
+ def publish!( a = {} )
272
+ generate! # -> png
273
+ end
274
+
275
+ def subgraph(name, a = {})
276
+ returning( SubGraph.new(name, a)) do |g|
277
+ @subgraphs << g
278
+ yield g if block_given?
279
+ end
280
+ end
281
+
282
+ def cluster(name = nil, a = {}, &block)
283
+ if name && name = "cluster_#{name}"
284
+ subgraph( name, a, &block )
285
+ else
286
+ clusters
287
+ end
288
+ end
289
+
290
+ def clusters
291
+ @subgraphs.select {|s| s.name =~ /^cluster_/ }.extend( Finder )
292
+ end
293
+
294
+ def initialize(name = 'my_graph', attrs = {})
295
+ @subgraphs = []
296
+ super( name, attrs )
297
+ yield self if block_given?
298
+ end
299
+ end
300
+ end
@@ -0,0 +1,115 @@
1
+ require File.expand_path("#{File.dirname(__FILE__)}/../helper")
2
+
3
+ ##
4
+ ##
5
+ ##
6
+
7
+ describe StateFu::Plotter do
8
+ include MySpecHelper
9
+ before do
10
+ reset!
11
+ make_pristine_class('Klass')
12
+ @machine = Klass.machine(:drawme) do
13
+ chain 'clean -tarnish-> dirty -fester-> putrid'
14
+ end
15
+ @machine = Klass.machine(:drawme)
16
+ end
17
+
18
+ it "deleteme" do
19
+ @machine.graphviz.save_as('/tmp/1.dot')
20
+ end
21
+
22
+ describe "example machine" do
23
+ it "should have 3 states" do
24
+ @machine.states.length.should == 3
25
+ end
26
+
27
+ it "should have 2 simple events" do
28
+ @machine.events.length.should == 2
29
+ @machine.events.each{ |e| e.should be_simple }
30
+ end
31
+
32
+ end
33
+
34
+ describe StateFu::Plotter do
35
+ describe "class methods" do
36
+ describe ".new" do
37
+ it "should expect a StateFu::Machine and return a Plotter" do
38
+ @plotter = StateFu::Plotter.new( @machine )
39
+ @plotter.should be_kind_of(StateFu::Plotter)
40
+ @plotter.machine.should == @machine
41
+ lambda { StateFu::Plotter.new( "abracadabra" ) }.should raise_error(RuntimeError)
42
+ end
43
+ end
44
+
45
+ describe "a new plotter" do
46
+ before do
47
+ @plotter = StateFu::Plotter.new( @machine )
48
+ end
49
+
50
+ it "should have an empty hash of states" do
51
+ @plotter = StateFu::Plotter.new( @machine )
52
+ @plotter.states.should == {}
53
+ end
54
+
55
+ end
56
+ end # class methods
57
+
58
+ describe "instance methods" do
59
+ before do
60
+ @plotter = StateFu::Plotter.new( @machine )
61
+ end
62
+
63
+ describe ".generate" do
64
+
65
+ it "should call generate_dot!" do
66
+ mock( @plotter ).generate_dot!() { "dot" }
67
+ @plotter.generate
68
+ end
69
+
70
+ it "should store the result in the dot attribute" do
71
+ mock( @plotter).generate_dot!() { "dot" }
72
+ @plotter.generate
73
+ @plotter.dot.should == "dot"
74
+ end
75
+
76
+ it "should return a Vizier::Graph" # .graph
77
+
78
+ describe ".save_as(filename)" do
79
+ it "should save the string to a file" do
80
+ mock( File).open( 'filename', 'w' ).yields( @fh = Object.new() )
81
+ mock( @fh ).write( @plotter.output )
82
+ @plotter.output.save_as( 'filename' )
83
+ end
84
+ end
85
+ describe ".save!" do
86
+ it "should save the string in a tempfile and return the path" do
87
+ mock(@tempfile = Object.new).path {"path"}.subject
88
+ mock(Tempfile).new(['state_fu_graph','.dot']).yields( @fh = Object.new() ) { @tempfile }
89
+ mock( @fh ).write( @plotter.output )
90
+ @plotter.output.save!.should == 'path'
91
+ end
92
+ end
93
+ end
94
+
95
+ describe "output" do
96
+ it "should return the result of .generate" do
97
+ @plotter.output.should == @plotter.generate
98
+ end
99
+ end
100
+
101
+ describe "generate_dot!" do
102
+
103
+ it "should return a string" do
104
+ @plotter.generate_dot!.should be_kind_of(String)
105
+ end
106
+
107
+ it "should extend the string to respond_to save_as" do
108
+ @plotter.output.should respond_to(:save_as)
109
+ end
110
+ end
111
+
112
+ end
113
+ end
114
+ end
115
+
@@ -0,0 +1,111 @@
1
+ require File.expand_path("#{File.dirname(__FILE__)}/../helper")
2
+
3
+ module MySpecHelper
4
+ module BindingExampleHelper
5
+
6
+ attr_accessor :ok
7
+
8
+ def helper_method
9
+ end
10
+
11
+ def requirement_satisfier?
12
+ true
13
+ end
14
+
15
+ def requirement_satisfier_with_arg?( t )
16
+ end
17
+ end
18
+
19
+ module OtherExampleHelper
20
+ def other_helper_method
21
+ end
22
+
23
+ def other_requirement_satisfier?
24
+ true
25
+ end
26
+ end
27
+ end
28
+
29
+ describe "extending bindings and transitions with Lathe#helper" do
30
+
31
+ include MySpecHelper
32
+
33
+ before(:each) do
34
+ reset!
35
+ make_pristine_class('Klass')
36
+ Klass.class_eval do
37
+ attr_accessor :ok
38
+ end
39
+
40
+ @machine = Klass.machine do
41
+ helper MySpecHelper::BindingExampleHelper
42
+ helper 'my_spec_helper/other_example_helper'
43
+
44
+ chain "a -a2b-> b -b2c-> c"
45
+
46
+ events.each do |e|
47
+ e.requires :requirement_satisfier?
48
+ e.requires :requirement_satisfier_with_arg?
49
+ e.requires :other_requirement_satisfier?
50
+ end
51
+ end
52
+
53
+ @other_machine = Klass.machine(:other) do
54
+ helper ::MySpecHelper::OtherExampleHelper
55
+ end
56
+ @obj = Klass.new
57
+ @binding = @obj.state_fu
58
+ @other_binding = @obj.other
59
+ @transition = @obj.state_fu.transition(:a2b)
60
+ end # before
61
+
62
+ #
63
+ #
64
+
65
+ describe "binding" do
66
+ describe "instance methods" do
67
+
68
+ it "should respond to helper_method" do
69
+ @binding.should respond_to( :helper_method)
70
+ end
71
+
72
+
73
+ it "should respond to other_helper_method" do
74
+ @binding.should respond_to( :other_helper_method)
75
+ end
76
+
77
+ it "should respond to requirement_satisfier?" do
78
+ @binding.should respond_to( :requirement_satisfier?)
79
+ end
80
+
81
+ it "should respond to other_requirement_satisfier?" do
82
+ @binding.should respond_to( :other_requirement_satisfier?)
83
+ end
84
+
85
+ end
86
+ end
87
+
88
+ describe "transition" do
89
+ describe "instance methods" do
90
+
91
+ it "should respond to helper_method" do
92
+ @transition.should respond_to( :helper_method)
93
+ end
94
+
95
+ it "should respond to other_helper_method" do
96
+ @transition.should respond_to( :other_helper_method)
97
+ end
98
+
99
+ it "should respond to requirement_satisfier?" do
100
+ @transition.should respond_to( :requirement_satisfier?)
101
+ end
102
+
103
+ it "should respond to other_requirement_satisfier?" do
104
+ @transition.should respond_to( :other_requirement_satisfier?)
105
+ end
106
+
107
+ end
108
+ end
109
+
110
+ end
111
+
@@ -0,0 +1,81 @@
1
+ require File.expand_path("#{File.dirname(__FILE__)}/../helper")
2
+
3
+ module RequirementFeatureHelper
4
+
5
+ def account_expired_test
6
+ false
7
+ end
8
+
9
+ def valid_password_test
10
+ true
11
+ end
12
+
13
+ def account_expired?
14
+ !!account_expired_test
15
+ end
16
+
17
+ def valid_password?
18
+ !!valid_password_test
19
+ end
20
+ end
21
+
22
+ describe "requirement objects" do
23
+ include MySpecHelper
24
+ before(:all) do
25
+ reset!
26
+ make_pristine_class('Klass')
27
+ Klass.machine do
28
+ helper RequirementFeatureHelper
29
+
30
+ initial_state :guest
31
+
32
+ event :login_success, :from => :guest, :to => [:logged_in, :expired] do
33
+ requires :valid_password?
34
+ end
35
+
36
+ event :login_failure, :from => :guest, :to => :guest do
37
+ execute :show_error
38
+ end
39
+
40
+ state :logged_in do
41
+ requires :not_account_expired?
42
+ end
43
+
44
+ state :expired do
45
+ requires :account_expired?
46
+ end
47
+ end
48
+ @obj = Klass.new
49
+ @binding = @obj.state_fu
50
+ end
51
+
52
+ describe "requirements with names beginning with not_" do
53
+
54
+ it "should return the opposite of the requirement name without not_" do
55
+ @binding.respond_to?(:valid_password?).should == true
56
+ @binding.respond_to?(:not_valid_password?).should == false
57
+ @binding.evaluate_named_proc_or_method( :valid_password? ).should == true
58
+ @binding.evaluate_named_proc_or_method( :not_valid_password? ).should == false
59
+ end
60
+
61
+ it "should call the method directly if one exists" do
62
+ mock( @binding ).not_valid_password?() { true }
63
+ @binding.evaluate_named_proc_or_method( :valid_password? ).should == true
64
+ @binding.evaluate_named_proc_or_method( :not_valid_password? ).should == true
65
+ end
66
+
67
+ it "should act as the opposite of requirement in guarding a transition" do
68
+ @binding.account_expired?.should == false
69
+ @binding.valid_password?.should == true
70
+ mock( @binding ).valid_password_test { false }
71
+ t = @binding.login_success(:logged_in)
72
+ t.requirements.should == [:not_account_expired?, :valid_password?]
73
+ t.unmet_requirements.should == [:valid_password?]
74
+ mock( @binding ).valid_password_test.times(2) { true }
75
+ t.unmet_requirements.should == []
76
+ @obj.login_success!(:logged_in).should == true
77
+ @binding.should == :logged_in
78
+ end
79
+ end
80
+
81
+ end
@@ -0,0 +1,47 @@
1
+ require File.expand_path("#{File.dirname(__FILE__)}/../helper")
2
+
3
+ describe "extending bindings and transitions with Lathe#helper" do
4
+
5
+ include MySpecHelper
6
+
7
+ before(:each) do
8
+ reset!
9
+ make_pristine_class('Klass')
10
+
11
+ @machine = Klass.machine do
12
+ state :normal, :colour => 'green'
13
+ state :bad, :colour => 'red'
14
+ event( :worsen, :colour => 'orange' ) { from :normal => :bad }
15
+ end
16
+ @obj = Klass.new
17
+ @binding = @obj.state_fu
18
+
19
+ end # before
20
+
21
+ describe "accessing sprocket options" do
22
+ describe "state#[]" do
23
+ it "should return state.options[key]" do
24
+ @machine.states[:normal][:colour].should == 'green'
25
+ end
26
+ end
27
+ describe "event#[]" do
28
+ it "should return event.options[key]" do
29
+ @machine.events[:worsen][:colour].should == 'orange'
30
+ end
31
+ end
32
+
33
+ describe "state#[]=" do
34
+ it "should update state.options" do
35
+ @machine.states[:normal][:flavour] = 'lime'
36
+ @machine.states[:normal][:flavour].should == 'lime'
37
+ end
38
+ end
39
+ describe "event#[]=" do
40
+ it "should update event.options" do
41
+ @machine.events[:worsen][:flavour] = 'orange'
42
+ @machine.events[:worsen][:flavour].should == 'orange'
43
+ end
44
+ end
45
+
46
+ end
47
+ end