state-fu 0.11.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 (85) hide show
  1. data/LICENSE +40 -0
  2. data/README.textile +293 -0
  3. data/Rakefile +114 -0
  4. data/lib/binding.rb +292 -0
  5. data/lib/event.rb +192 -0
  6. data/lib/executioner.rb +120 -0
  7. data/lib/hooks.rb +39 -0
  8. data/lib/interface.rb +132 -0
  9. data/lib/lathe.rb +538 -0
  10. data/lib/machine.rb +184 -0
  11. data/lib/method_factory.rb +243 -0
  12. data/lib/persistence.rb +116 -0
  13. data/lib/persistence/active_record.rb +34 -0
  14. data/lib/persistence/attribute.rb +47 -0
  15. data/lib/persistence/base.rb +100 -0
  16. data/lib/persistence/relaxdb.rb +23 -0
  17. data/lib/persistence/session.rb +7 -0
  18. data/lib/sprocket.rb +58 -0
  19. data/lib/state-fu.rb +56 -0
  20. data/lib/state.rb +48 -0
  21. data/lib/support/active_support_lite/array.rb +9 -0
  22. data/lib/support/active_support_lite/array/access.rb +60 -0
  23. data/lib/support/active_support_lite/array/conversions.rb +202 -0
  24. data/lib/support/active_support_lite/array/extract_options.rb +21 -0
  25. data/lib/support/active_support_lite/array/grouping.rb +109 -0
  26. data/lib/support/active_support_lite/array/random_access.rb +13 -0
  27. data/lib/support/active_support_lite/array/wrapper.rb +25 -0
  28. data/lib/support/active_support_lite/blank.rb +67 -0
  29. data/lib/support/active_support_lite/cattr_reader.rb +57 -0
  30. data/lib/support/active_support_lite/keys.rb +57 -0
  31. data/lib/support/active_support_lite/misc.rb +59 -0
  32. data/lib/support/active_support_lite/module.rb +1 -0
  33. data/lib/support/active_support_lite/module/delegation.rb +130 -0
  34. data/lib/support/active_support_lite/object.rb +9 -0
  35. data/lib/support/active_support_lite/string.rb +38 -0
  36. data/lib/support/active_support_lite/symbol.rb +16 -0
  37. data/lib/support/applicable.rb +41 -0
  38. data/lib/support/arrays.rb +197 -0
  39. data/lib/support/core_ext.rb +90 -0
  40. data/lib/support/exceptions.rb +106 -0
  41. data/lib/support/has_options.rb +16 -0
  42. data/lib/support/logger.rb +165 -0
  43. data/lib/support/methodical.rb +17 -0
  44. data/lib/support/no_stdout.rb +55 -0
  45. data/lib/support/plotter.rb +62 -0
  46. data/lib/support/vizier.rb +300 -0
  47. data/lib/tasks/spec_last.rake +55 -0
  48. data/lib/tasks/state_fu.rake +57 -0
  49. data/lib/transition.rb +338 -0
  50. data/lib/transition_query.rb +224 -0
  51. data/spec/custom_formatter.rb +49 -0
  52. data/spec/features/binding_and_transition_helper_mixin_spec.rb +111 -0
  53. data/spec/features/method_missing_only_once_spec.rb +28 -0
  54. data/spec/features/not_requirements_spec.rb +118 -0
  55. data/spec/features/plotter_spec.rb +97 -0
  56. data/spec/features/shared_log_spec.rb +7 -0
  57. data/spec/features/singleton_machine_spec.rb +39 -0
  58. data/spec/features/state_and_array_options_accessor_spec.rb +47 -0
  59. data/spec/features/transition_boolean_comparison_spec.rb +101 -0
  60. data/spec/helper.rb +13 -0
  61. data/spec/integration/active_record_persistence_spec.rb +202 -0
  62. data/spec/integration/binding_extension_spec.rb +41 -0
  63. data/spec/integration/class_accessor_spec.rb +117 -0
  64. data/spec/integration/event_definition_spec.rb +74 -0
  65. data/spec/integration/example_01_document_spec.rb +133 -0
  66. data/spec/integration/example_02_string_spec.rb +88 -0
  67. data/spec/integration/instance_accessor_spec.rb +97 -0
  68. data/spec/integration/lathe_extension_spec.rb +67 -0
  69. data/spec/integration/machine_duplication_spec.rb +101 -0
  70. data/spec/integration/relaxdb_persistence_spec.rb +97 -0
  71. data/spec/integration/requirement_reflection_spec.rb +270 -0
  72. data/spec/integration/state_definition_spec.rb +163 -0
  73. data/spec/integration/transition_spec.rb +1033 -0
  74. data/spec/spec.opts +9 -0
  75. data/spec/spec_helper.rb +132 -0
  76. data/spec/state_fu_spec.rb +948 -0
  77. data/spec/units/binding_spec.rb +192 -0
  78. data/spec/units/event_spec.rb +214 -0
  79. data/spec/units/exceptions_spec.rb +82 -0
  80. data/spec/units/lathe_spec.rb +570 -0
  81. data/spec/units/machine_spec.rb +229 -0
  82. data/spec/units/method_factory_spec.rb +366 -0
  83. data/spec/units/sprocket_spec.rb +69 -0
  84. data/spec/units/state_spec.rb +59 -0
  85. metadata +171 -0
@@ -0,0 +1,62 @@
1
+ require 'tempfile'
2
+
3
+ module StateFu
4
+ class Plotter
5
+ attr_reader :machine, :dot, :graph, :states, :events
6
+
7
+ OUTPUT_HELPER = Module.new do
8
+
9
+ def save!
10
+ Tempfile.new(['state_fu_graph','.dot']) do |fh|
11
+ fh.write( self )
12
+ end.path
13
+ end
14
+
15
+ def save_as( filename )
16
+ File.open(filename, 'w') { |fh| fh.write( self ) }
17
+ end
18
+
19
+ def save_png(filename)
20
+ raise NotImplementedError
21
+ # dot graph.dot -Tpng -O
22
+ end
23
+
24
+ end
25
+
26
+ def output
27
+ generate
28
+ end
29
+
30
+ def initialize( machine, options={} )
31
+ raise RuntimeError, machine.class.to_s unless machine.is_a?(StateFu::Machine)
32
+ @machine = machine
33
+ @options = options.symbolize_keys!
34
+ @states = {}
35
+ @events = {}
36
+ # generate
37
+ end
38
+
39
+ def generate
40
+ @dot ||= generate_dot!.extend( OUTPUT_HELPER )
41
+ end
42
+
43
+ def generate_dot!
44
+ @graph = Vizier::Graph.new(:state_machine) do |g|
45
+ g.node :shape => 'doublecircle'
46
+ machine.state_names.map.each do |s|
47
+ @states[s] = g.add_node(s.to_s)
48
+ end
49
+ machine.events.map.each do |e|
50
+ e.origins.map(&:name).each do |from|
51
+ e.targets.map(&:name).each do |to|
52
+ g.connect( @states[from], @states[to], :label => e.name.to_s )
53
+ end
54
+ end
55
+ # @events[s] = g.add_node(s.to_s)
56
+ end
57
+ end
58
+ @graph.generate!
59
+ end
60
+
61
+ end
62
+ end
@@ -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
+ # Vizier is a simple library to help generate dot output for graphviz. It is used by StateFu's rake
10
+ # tasks to generate graphs of state machines.
11
+ #
12
+ # Sorry, there's only Heisendocumentation (if I realize anyone's looking for docs, I might write some)
13
+ #
14
+ module Vizier #:nodoc:all
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,55 @@
1
+ require 'fileutils'
2
+
3
+ unless Object.const_defined?('STATE_FU_APP_PATH')
4
+ STATE_FU_APP_PATH = Object.const_defined?('RAILS_ROOT') ? RAILS_ROOT : File.join( File.dirname(__FILE__), '/../..')
5
+ end
6
+
7
+ unless Object.const_defined?('STATE_FU_PLUGIN_PATH')
8
+ STATE_FU_PLUGIN_PATH = Object.const_defined?('RAILS_ROOT') ? File.join( RAILS_ROOT, '/vendor/plugins/state-fu' ) : STATE_FU_APP_PATH
9
+ end
10
+
11
+ begin
12
+ require 'rake'
13
+ require 'spec'
14
+ require 'spec/rake/spectask'
15
+
16
+ namespace :spec do
17
+ def find_last_modified_spec
18
+ require 'find'
19
+ specs = []
20
+ Find.find( File.expand_path(File.join(STATE_FU_APP_PATH,'spec'))) do |f|
21
+ next unless f !~ /\.#/ && f =~ /_spec.rb$/
22
+ specs << f
23
+ end
24
+ spec = specs.sort_by { |spec| File.stat( spec ).mtime }.last
25
+ end
26
+
27
+ desc "runs the last modified spec; L=n runs only that line"
28
+ Spec::Rake::SpecTask.new(:last) do |t|
29
+ specfile = find_last_modified_spec || return
30
+ t.verbose = true
31
+ t.spec_opts = ["-c","-b","-u"]
32
+ if ENV['L']
33
+ t.spec_opts += ["-l", ENV["L"],"-f", "specdoc"]
34
+ else
35
+ t.spec_opts += ["-f", "profile"]
36
+ end
37
+ t.spec_files = FileList[specfile]
38
+ end
39
+
40
+ desc "runs all specs, or those which last failed"
41
+ Spec::Rake::SpecTask.new(:faily) do |t|
42
+ specfile = find_last_modified_spec || return
43
+ faily = 'spec.fail'
44
+ t.verbose = true
45
+ t.spec_opts = ["-f","failing_examples:#{faily}", "-f","n","-c","-b","-u"]
46
+ if File.exists?(faily) && File.read(faily).split("\n")[0] != ""
47
+ t.spec_opts << ["-e",faily]
48
+ end
49
+ end
50
+ end
51
+
52
+ rescue LoadError
53
+ # fail quietly if rspec is not installed
54
+ end
55
+
@@ -0,0 +1,57 @@
1
+ require 'fileutils'
2
+
3
+ unless Object.const_defined?('STATE_FU_APP_PATH')
4
+ STATE_FU_APP_PATH = Object.const_defined?('RAILS_ROOT') ? RAILS_ROOT : File.join( File.dirname(__FILE__), '/../..')
5
+ end
6
+
7
+ unless Object.const_defined?('STATE_FU_PLUGIN_PATH')
8
+ STATE_FU_PLUGIN_PATH = Object.const_defined?('RAILS_ROOT') ? File.join( RAILS_ROOT, '/vendor/plugins/state-fu' ) : STATE_FU_APP_PATH
9
+ end
10
+
11
+ namespace :state_fu do
12
+
13
+ task :update do
14
+ path = STATE_FU_PLUGIN_PATH
15
+ pwd = FileUtils.pwd
16
+ FileUtils.cd( path )
17
+ system('git pull')
18
+ FileUtils.cd pwd
19
+ end
20
+
21
+ def graph_name( klass, machine, doc_path = false )
22
+ parts = ["#{klass}_#{machine}"]
23
+ if doc_path
24
+ folder = parts.unshift( File.join( STATE_FU_APP_PATH, "doc/") )
25
+ FileUtils.mkdir_p( folder )
26
+ parts.push( '.png' )
27
+ end
28
+ parts.join
29
+ end
30
+
31
+ def graph( klass, machine )
32
+ name = graph_name( klass, machine )
33
+ graphviz = `which dot`.strip || raise("Graphviz not installed? Can't find dot executable!")
34
+ puts graphviz
35
+ tmp_dot = "/tmp/#{name}.dot"
36
+ klass.machine( machine.to_sym ).graphviz.save_as( tmp_dot )
37
+ tmp_png = tmp_dot + '.png'
38
+ doc_png = graph_name( klass, machine, true )
39
+ puts( "#{graphviz} -Tpng -O #{tmp_dot}" )
40
+ system( "#{graphviz} -Tpng -O #{tmp_dot}" )
41
+ FileUtils.cp tmp_png, doc_png
42
+ doc_png
43
+ end
44
+
45
+ desc "Graph workflows with dot"
46
+ task :graph => :environment do |t|
47
+ state_fu_classes = ObjectSpace.each_object { |o| x << o if o.respond_to? :machines }
48
+ state_fu_classes.each do |klass|
49
+ klass.state_fu_machines.each do |machine_name, machine|
50
+ STDERR.puts "#{klass} -> #{machine_name.inspect}"
51
+ doc_png = graph( klass, machine_name )
52
+ # yield doc_png if block_given?
53
+ end
54
+ end
55
+ # `open #{doc_png}`
56
+ end
57
+ end