state-fu 0.11.1

Sign up to get free protection for your applications and to get access to all the features.
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
data/LICENSE ADDED
@@ -0,0 +1,40 @@
1
+ # State Fu
2
+ #
3
+ # The original master repository for State-Fu is at github:
4
+ # http://github.com/davidlee/state-fu
5
+ #
6
+ # Original Author: David Lee (2009)
7
+ # http://github.com/davidlee
8
+ #
9
+ # Thanks to: Ryan Allen - ryan-allen/workflow
10
+ # John Barnette - jbarnett/stateful
11
+ # Scott Barron - rubyist/aasm
12
+ #
13
+ # and other rubyists too numerous to mention, for the inspiration
14
+ #
15
+ # This software is released under this BSD license:
16
+ #
17
+ # Copyright (c) 2009, David Lee. All rights reserved.
18
+ #
19
+ # Redistribution and use in source and binary forms, with or without
20
+ # modification, are permitted provided that the following conditions
21
+ # are met:
22
+ #
23
+ # * Redistributions of source code must retain the above copyright
24
+ # notice, this list of conditions and the following disclaimer.
25
+ # * Redistributions in binary form must reproduce the above copyright
26
+ # notice, this list of conditions and the following disclaimer in the
27
+ # documentation and/or other materials provided with the distribution.
28
+ #
29
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
30
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
31
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
32
+ # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
33
+ # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
34
+ # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
35
+ # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
36
+ # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
37
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
38
+ # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
39
+ # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
40
+ # POSSIBILITY OF SUCH DAMAGE.
data/README.textile ADDED
@@ -0,0 +1,293 @@
1
+ h1. StateFu
2
+
3
+ h2. What is it?
4
+
5
+ StateFu is another Ruby state machine.
6
+
7
+ h2. What is a state machine?
8
+
9
+ Finite state machines are a model for program behaviour; like
10
+ object-oriented programming, they provide an abstract way to think
11
+ about a domain.
12
+
13
+ In a finite state machine, there are a number of discrete states. Only
14
+ one state may be occupied at any given time (hence the "finite").
15
+
16
+ States are linked together by events, and there are rules which govern
17
+ when or how transitions between states can occur. Actions may be fired
18
+ on entry to or exit from a state, or when a certain transition occurs.
19
+
20
+ h2. Why is StateFu different to the other twenty state machines for Ruby?
21
+
22
+ State machines are potentially a powerful way to simplify and
23
+ structure a lot of problems. They can be used to:
24
+
25
+ * succinctly define the grammar of a networking protocol or a
26
+ configuration DSL
27
+
28
+ * clearly and compactly describe complex logic, which might otherwise
29
+ be difficult to understand by playing "follow the rabbit" through
30
+ methods thick with implementation details
31
+
32
+ * serialize and process multiple revisions of changing business rules
33
+
34
+ * provide an abstract representation of program or domain behaviour
35
+ which can be introspected, edited, queried and executed on the fly,
36
+ in a high-level and human-readable format
37
+
38
+ * provide a straightforward and easy way to record and validate
39
+ "status" information, especially when there are rules governing
40
+ when and how it can be updated
41
+
42
+ * reduce proliferation of classes and modules, or easily define and
43
+ control functionally related groups of objects, by defining
44
+ behaviours on interacting components of a state machine
45
+
46
+ * elegantly implement simple building blocks like stacks / queues,
47
+ parsers, schedulers, automata, etc
48
+
49
+ StateFu was written from the ground up with one goal in mind: to be
50
+ over-engineered. It is designed to make truly ambitious use of state
51
+ machines not only viable, but strongly advantageous in many situations.
52
+
53
+ It is designed in the very opposite vein to the intentional minimalism
54
+ of most ruby state machine projects; it is tasked with taking on a
55
+ great deal of complexity and functionality, and abstracting it behind
56
+ a nice DSL, so that the code which *you* have to maintain is shorter and
57
+ clearer.
58
+
59
+ StateFu allows you to:
60
+
61
+ * give a class any number of machines
62
+
63
+ * define behaviours on state entry / exit; before, after or during
64
+ execution of a particular event; or before / after every transition
65
+ in a given machine
66
+
67
+ * give any object own its own private, "singleton" machines,
68
+ which are unique to that object and modifiable at runtime.
69
+
70
+ * create events with any number of origin or target states
71
+
72
+ * define and query guard conditions / transition requirements,
73
+ to establish rules about when a transition is valid
74
+
75
+ * use powerful reflection and logging capabilities to easily expose
76
+ and debug the operation of your machines
77
+
78
+ * automatically and unobtrusively define methods for querying each
79
+ state and event, and for firing transitions
80
+
81
+ * easily find out which transitions are valid at any given time
82
+
83
+ * generate descriptive, contextual messages when a transition is
84
+ invalid
85
+
86
+ * halt a transition during execution
87
+
88
+ * easily extend StateFu's DSL to match the problem domain
89
+
90
+ * fire transitions with a payload of arguments and program context,
91
+ which is available to guard conditions, event hooks, and
92
+ requirement messages when they are evaluated
93
+
94
+ * use a lovely, simple and flexible API which gives you plenty of
95
+ choices about how to describe your problem domain; choose (or
96
+ build) a programming style which suits the task at hand from an
97
+ expressive range of options
98
+
99
+ * store arbitrary meta-data on any component of StateFu - a simple
100
+ but extremely powerful tool for integration with almost anything.
101
+
102
+ * flexible and helpful logging out of the box - will use the Rails
103
+ logger if you're in a Rails project, or standalone logging to
104
+ STDOUT or a file. Configurable loglevel and message prefixes help
105
+ StateFu be a good citizen in a shared application log.
106
+
107
+ * automatically generate diagrams of state machines / workflows with graphviz
108
+
109
+ * use an ActiveRecord field for state persistence, or a regular
110
+ attribute - or use both, on the same class, for different
111
+ machines. If an appropriate ActiveRecord field exists for a
112
+ machine, it will be used. Otherwise, an attr_accessor will be used
113
+ (and created, if necessary).
114
+
115
+ * customising the persistence mechanism (eg to use a Rails session,
116
+ or a text file, or your choice of ORM) is usually as easy as
117
+ defining a getter and setter method for the persistence field, and
118
+ a rule about when to use it. If you want to use StateFu with a
119
+ persistence mechanism which is not yet supported, send me a message.
120
+
121
+ * StateFu is fast, lightweight and useful enough to use in any ruby
122
+ project - works with Rails but does not require it.
123
+
124
+ h2. Still not sold?
125
+
126
+ StateFu is forged from a reassuringly dense but unidentifiable metal
127
+ which comes only from the rarest of meteorites, and it ticks when you
128
+ hold it up to your ear.[1]
129
+
130
+ It is elegant, powerful and transparent enough that you can use
131
+ it to drive substantial parts of your application, and actually want
132
+ to do so.
133
+
134
+ It is designed as a library for authors, as well as users, of
135
+ libraries: StateFu goes to great lengths to impose very few limits on
136
+ your ability to introspect, manipulate and extend the core features.
137
+
138
+ It is also delightfully elegant and easy to use for simple things:
139
+
140
+ <pre><code>
141
+
142
+ class Document < ActiveRecord::Base
143
+ include StateFu
144
+
145
+ def update_rss
146
+ puts "new feed!"
147
+ # ... do something here
148
+ end
149
+
150
+ machine( :status ) do
151
+ state :draft do
152
+ event :publish, :to => :published
153
+ end
154
+
155
+ state :published do
156
+ on_entry :update_rss
157
+ requires :author # a database column
158
+ end
159
+
160
+ event :delete, :from => :ALL, :to => :deleted do
161
+ execute :destroy
162
+ end
163
+
164
+ # save all states once transition is complete.
165
+ # this wants to be last, as it iterates over each state which is
166
+ # already defined.
167
+ states do
168
+ accepted { object.save! }
169
+ end
170
+ end
171
+ end
172
+
173
+ my_doc = Document.new
174
+
175
+ my_doc.status # returns a StateFu::Binding, which lets us access the 'Fu
176
+ my_doc.status.state => 'draft' # if this wasn't already a database column or attribute, an
177
+ # attribute has been created to keep track of the state
178
+ my_doc.status.name => :draft # the name of the current_state (defaults to the first defined)
179
+ my_doc.status.publish! # raised => StateFu::RequirementError: [:author]
180
+ # the author requirement prevented the transition
181
+ my_doc.status.name => :draft # see? still a draft.
182
+ my_doc.author = "Susan" # so let's satisfy it ...
183
+ my_doc.publish! # and try again.
184
+ "new feed!" # aha - our event hook fires!
185
+ my_doc.status.name => :published # and the state has been updated.
186
+
187
+ </code></pre>
188
+
189
+ StateFu works with any modern Ruby ( 1.8.6, 1.8.7, and 1.9.1)
190
+
191
+ h2. Getting started
192
+
193
+ You can either clone the repository in the usual fashion (eg to
194
+ yourapp/vendor/plugins/state-fu), or use StateFu as a gem.
195
+
196
+ To install as a gem:
197
+
198
+ <pre>
199
+ <code>
200
+ gem install davidlee-state-fu -s http://gems.github.com
201
+ </code>
202
+ </pre>
203
+
204
+ To require it in your ruby project:
205
+
206
+ <pre>
207
+ <code>
208
+ require 'rubygems'
209
+ require 'state-fu'
210
+ </code>
211
+ </pre>
212
+
213
+ To install the dependencies for running specs:
214
+
215
+ <pre>
216
+ <code>
217
+ sudo gem install rspec rr
218
+ rake # run the specs
219
+ rake spec:doc # generate specdocs
220
+ rake doc # generate rdocs
221
+ rake build # build the gem locally
222
+ rake install # install it
223
+ </code>
224
+ </pre>
225
+
226
+ Now you can simply <code>include StateFu</code> in any class you wish to make stateful.
227
+
228
+ The spec/ and features/ folders are currently one of the best source
229
+ of documentation. The documentation is gradually evolving to catch up
230
+ with the features, but if you have any questions I'm happy to help you
231
+ get started.
232
+
233
+ If you have questions, feature request or ideas, please join the
234
+ "google group":http://groups.google.com/group/state-fu or send me a
235
+ message on GitHub.
236
+
237
+ h3. A note about ActiveSupport
238
+
239
+ StateFu will use ActiveSupport if it is already loaded. If not, it
240
+ will load its own (heavily trimmed) 'lite' version.
241
+
242
+ In most projects this will behave transparently, but it does mean that
243
+ if you require StateFu *before* other libraries which
244
+ require ActiveSupport (e.g. ActiveRecord), you may have to
245
+ explicitly <code>require 'activesupport'</code> before loading the
246
+ dependent libraries.
247
+
248
+ So if you plan to use ActiveSupport in a stand-alone project with
249
+ StateFu, you should require it before StateFu.
250
+
251
+
252
+ h3. Addditional Resources
253
+
254
+ Also see the "issue tracker":http://github.com/davidlee/state-fu/issues
255
+
256
+ And the "build monitor":http://runcoderun.com/davidlee/state-fu/
257
+
258
+ And the "RDoc":http://rdoc.info/projects/davidlee/state-fu
259
+
260
+
261
+ h3. StateFu is not a complete BPM (Business Process Management) platform
262
+
263
+ It's worth noting that StateFu is at it's core a state machine, which
264
+ strives to be powerful enough to be able to drive many kinds of
265
+ application behaviour.
266
+
267
+ It is not, however, a classical workflow engine on par with Ruote. In
268
+ StateFu the basic units with which "workflows" are built are states
269
+ and events; Ruote takes a higher level view, dealing with processes
270
+ and participants. As a result, it's capable of directly implementing
271
+ these design patterns:
272
+
273
+ http://openwferu.rubyforge.org/patterns.html
274
+
275
+ Whereas StateFu cannot, for example, readily model forking / merging
276
+ of processes (nor does it handles scheduling, process management, etc.
277
+
278
+ The author of Ruote, the Ruby Workflow Engine, outlines the difference
279
+ pretty clearly here:
280
+
281
+ http://jmettraux.wordpress.com/2009/07/03/state-machine-workflow-engine/
282
+
283
+ If your application can be described with StateFu, you'll likely find
284
+ it simpler to get running and work with; if not, you may find Ruote,
285
+ or a combination of the two, suits your needs perfectly.
286
+
287
+ h3. Thanks
288
+
289
+ * dsturnbull, for patches
290
+
291
+ * lachie, benkimball for pointing out README bugs / typos
292
+
293
+ * Ryan Allen for his original Workflow library
data/Rakefile ADDED
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env ruby
2
+ require "spec/rake/spectask"
3
+ #require 'cucumber/rake/task'
4
+ require "date"
5
+ require "fileutils"
6
+ require "rubygems"
7
+
8
+ load File.join( File.dirname(__FILE__),"/lib/tasks/state_fu.rake" )
9
+
10
+ module Rakefile
11
+ def self.windows?
12
+ /djgpp|(cyg|ms|bcc)win|mingw/ =~ RUBY_PLATFORM
13
+ end
14
+ end
15
+
16
+ load 'lib/tasks/spec_last.rake'
17
+ load 'lib/tasks/state_fu.rake'
18
+
19
+ # to build the gem:
20
+ #
21
+ # gem install jeweller
22
+ # rake build
23
+ # rake install
24
+ begin
25
+ require 'jeweler'
26
+ Jeweler::Tasks.new do |s| # gemspec (Gem::Specification)
27
+ s.name = "state-fu"
28
+ s.rubyforge_project = "state-fu"
29
+ s.platform = Gem::Platform::RUBY
30
+ s.has_rdoc = true
31
+ # s.extra_rdoc_files = ["README.rdoc"]
32
+ s.summary = "A rich library for state-oriented programming with state machines / workflows"
33
+ s.description = s.summary
34
+ s.author = "David Lee"
35
+ s.email = "david@rubyist.net.au"
36
+ s.homepage = "http://github.com/davidlee/state-fu"
37
+ s.require_path = "lib"
38
+ # s.files = %w(README.rdoc Rakefile) + Dir.glob("{lib,spec}/**/*")
39
+ s.files = %w(Rakefile) + Dir.glob("{lib,spec}/**/*")
40
+ end
41
+ rescue LoadError
42
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
43
+ end
44
+
45
+ namespace :spec do
46
+
47
+ desc 'run the nice new specs'
48
+ Spec::Rake::SpecTask.new(:state_fu) do |t|
49
+ t.spec_files = FileList["spec/state_fu_spec.rb"]
50
+ t.spec_opts = ["--options", "spec/spec.opts"]
51
+ end
52
+
53
+
54
+ desc "Run all (old) specs"
55
+ Spec::Rake::SpecTask.new(:all) do |t|
56
+ t.spec_files = FileList["spec/**/*_spec.rb"]
57
+ t.spec_opts = ["--options", "spec/spec.opts"]
58
+ end
59
+
60
+ task :skip_slow do
61
+ ENV['SKIP_SLOW_SPECS'] = 'true'
62
+ end
63
+
64
+ desc "Run all specs, except especially slow ones"
65
+ task :quick => [:skip_slow, :all]
66
+
67
+ desc "Run all specs with profiling & backtrace"
68
+ Spec::Rake::SpecTask.new(:prof) do |t|
69
+ t.spec_files = FileList["spec/**/*_spec.rb"]
70
+ t.spec_opts = ['-c','-b','-u','-f','profile','-R','-L','mtime']
71
+ end
72
+
73
+ desc "Print Specdoc for all specs (eaxcluding plugin specs)"
74
+ Spec::Rake::SpecTask.new(:doc) do |t|
75
+ t.spec_files = FileList["spec/**/*_spec.rb"]
76
+ t.spec_opts = ["--format", "nested","--color"]
77
+ end
78
+
79
+ desc "Run autotest"
80
+ task :auto do |t|
81
+ exec 'autospec'
82
+ end
83
+
84
+ task :default => :state_fu
85
+ end
86
+
87
+ desc 'Runs irb in this project\'s context'
88
+ task :irb do |t|
89
+ exec 'irb -I lib -I spec -r state-fu -r spec_helper'
90
+ end
91
+
92
+ desc 'Runs rdoc on the project lib directory'
93
+ task :doc do |t|
94
+ exec 'rdoc lib/'
95
+ end
96
+
97
+ desc 'Delete logfiles'
98
+ namespace :log do
99
+ task :clear do |t|
100
+ Dir['log/*.log'].each { |log| File.rm(log) }
101
+ end
102
+ end
103
+
104
+ begin
105
+ require 'cucumber/rake/task'
106
+
107
+ Cucumber::Rake::Task.new(:features) do |t|
108
+ t.cucumber_opts = "--format pretty"
109
+ end
110
+ rescue LoadError => e
111
+ end
112
+
113
+ task :all => 'spec:all'
114
+ task :default => :all
data/lib/binding.rb ADDED
@@ -0,0 +1,292 @@
1
+ module StateFu
2
+ class Binding
3
+
4
+ attr_reader :object, :machine, :method_name, :field_name, :persister, :transitions, :options, :target
5
+
6
+
7
+ # the constructor should not be called manually; a binding is
8
+ # returned when an instance of a class with a StateFu::Machine
9
+ # calls:
10
+ #
11
+ # instance.#state_fu (for the default machine which is called :state_fu),
12
+ # instance.#state_fu( :<machine_name> ) ,or
13
+ # instance.#<machine_name>
14
+ #
15
+ def initialize( machine, object, method_name, options={} )
16
+ @machine = machine
17
+ @object = object
18
+ @method_name = method_name
19
+ @transitions = []
20
+ @options = options.symbolize_keys!
21
+ @target = singleton? ? object : object.class
22
+ @field_name = options[:field_name] || @target.state_fu_field_names[method_name]
23
+ @persister = Persistence.for( self )
24
+
25
+ # define event methods on this binding and its @object
26
+ MethodFactory.new( self ).install!
27
+ @machine.helpers.inject_into( self )
28
+ end
29
+
30
+ alias_method :o, :object
31
+ alias_method :obj, :object
32
+ alias_method :model, :object
33
+ alias_method :instance, :object
34
+
35
+ alias_method :workflow, :machine
36
+ alias_method :state_machine, :machine
37
+
38
+ #
39
+ # current state
40
+ #
41
+
42
+ # the current State
43
+ def current_state
44
+ persister.current_state
45
+ end
46
+ alias_method :now, :current_state
47
+ alias_method :state, :current_state
48
+
49
+ # the name, as a Symbol, of the binding's current_state
50
+ def current_state_name
51
+ begin
52
+ current_state.name.to_sym
53
+ rescue NoMethodError
54
+ nil
55
+ end
56
+ end
57
+ alias_method :name, :current_state_name
58
+ alias_method :state_name, :current_state_name
59
+ alias_method :to_sym, :current_state_name
60
+
61
+ #
62
+ # These methods are called from methods defined by MethodFactory.
63
+ #
64
+
65
+ # event_name [target], *args
66
+ #
67
+ def find_transition(event, target=nil, *args)
68
+ target ||= args.last[:to].to_sym rescue nil
69
+ query = transitions.for_event(event).to(target).with(*args)
70
+ query.find || query.valid.singular || nil
71
+ end
72
+
73
+ # event_name? [target], *args
74
+ #
75
+ def can_transition?(event, target=nil, *args)
76
+ begin
77
+ if t = find_transition(event, target, *args)
78
+ t.valid?(*args)
79
+ end
80
+ rescue IllegalTransition, UnknownTarget
81
+ nil
82
+ end
83
+ end
84
+
85
+ # event_name! [target], *args
86
+ #
87
+ def fire_transition!(event, target=nil, *args)
88
+ find_transition(event, target, *args).fire!
89
+ end
90
+
91
+ #
92
+ # events
93
+ #
94
+
95
+ # returns a list of Events which can fire from the current_state
96
+ def events
97
+ machine.events.select do |e|
98
+ e.can_transition_from? current_state
99
+ end.extend EventArray
100
+ end
101
+ alias_method :events_from_current_state, :events
102
+
103
+ # all states which can be reached from the current_state.
104
+ # Does not check transition requirements, etc.
105
+ def next_states
106
+ events.map(&:targets).compact.flatten.uniq.extend StateArray
107
+ end
108
+
109
+ #
110
+ # transition validation
111
+ #
112
+
113
+ def transitions(opts={}) # .with(*args)
114
+ TransitionQuery.new(self, opts)
115
+ end
116
+
117
+ def valid_transitions(*args)
118
+ transitions.valid.with(*args)
119
+ end
120
+
121
+ def valid_next_states(*args)
122
+ valid_transitions(*args).targets
123
+ end
124
+
125
+ def valid_events(*args)
126
+ valid_transitions(*args).events
127
+ end
128
+
129
+ def invalid_events(*args)
130
+ (events - valid_events(*args)).extend StateArray
131
+ end
132
+
133
+
134
+ # initializes a new Transition to the given destination, with the
135
+ # given *args (to be passed to requirements and hooks).
136
+ #
137
+ # If a block is given, it yields the Transition or is executed in
138
+ # its evaluation context, depending on the arity of the block.
139
+ def transition( event_or_array, *args, &block )
140
+ return transitions.with(*args, &block).find(event_or_array)
141
+ end
142
+
143
+ #
144
+ # next_transition and friends: when there's exactly one valid move
145
+ #
146
+
147
+ # if there is exactly one legal & valid transition which can be fired with
148
+ # the given (optional) arguments, return it.
149
+ def next_transition( *args, &block )
150
+ transitions.with(*args, &block).next
151
+ end
152
+
153
+ # as above but ignoring any transitions whose origin and target are the same
154
+ def next_transition_excluding_cycles( *args, &block )
155
+ transitions.not_cyclic.with(*args, &block).next
156
+ end
157
+
158
+ # if there is exactly one state reachable via a transition which
159
+ # is valid with the given optional arguments, return it.
160
+ def next_state(*args, &block)
161
+ transitions.with(*args, &block).next_state
162
+ end
163
+
164
+ # if there is exactly one event which is valid with the given
165
+ # optional arguments, return it
166
+ def next_event( *args )
167
+ transitions.with(*args, &block).next_event
168
+ end
169
+
170
+ # if there is a next_transition, create, fire & return it
171
+ # otherwise raise an IllegalTransition
172
+ def next!( *args, &block )
173
+ if t = next_transition( *args, &block )
174
+ t.fire!
175
+ else
176
+ raise TransitionNotFound.new( self, valid_transitions(*args), "Exactly 1 valid transition required.")
177
+ end
178
+ end
179
+ alias_method :next_transition!, :next!
180
+ alias_method :next_event!, :next!
181
+ alias_method :next_state!, :next!
182
+
183
+ # if there is a next_transition, return true / false depending on
184
+ # whether its requirements are met
185
+ # otherwise, nil
186
+ def next?( *args, &block )
187
+ if t = next_transition( *args, &block )
188
+ t.requirements_met?
189
+ end
190
+ end
191
+ # alias_method :next_state?, :next?
192
+ # alias_method :next_event?, :next?
193
+
194
+ # Cyclic transitions (origin == target)
195
+
196
+ # if there is one possible cyclical event, return a transition there
197
+ # otherwise, maybe we got an event name as an argument?
198
+ def cycle(event_or_array=nil, *args, &block)
199
+ if event_or_array.nil?
200
+ transitions.cyclic.with(*args, &block).singular ||
201
+ transitions.cyclic.with(*args, &block).valid.singular
202
+ else
203
+ transitions.cyclic.with(*args, &block).find(event_or_array)
204
+ end
205
+ end
206
+
207
+ # if there is a single possible cycle() transition, fire and return it
208
+ # otherwise raise an IllegalTransition
209
+ def cycle!(event_or_array=nil, *args, &block )
210
+ returning cycle(event_or_array, *args, &block ) do |t|
211
+ raise TransitionNotFound.new( self, transitions.cyclic.with(*args,&block), "Cannot cycle! unless there is exactly one cyclic event") \
212
+ if t.nil?
213
+ t.fire!
214
+ end
215
+ end
216
+
217
+ # if there is one possible cyclical event, evaluate its
218
+ # requirements (true/false), else nil
219
+ def cycle?(event_or_array=nil, *args )
220
+ if t = cycle(event_or_array, *args )
221
+ t.requirements_met?
222
+ end
223
+ end
224
+
225
+ # next! without the raise if there's no next transition
226
+ # TODO SPECME
227
+ def update!( *args, &block )
228
+ if t = next_transition( *args, &block )
229
+ t.fire!
230
+ end
231
+ end
232
+
233
+ #
234
+ # misc
235
+ #
236
+
237
+ # change the current state of the binding without any
238
+ # requirements or other sanity checks, or any hooks firing.
239
+ # Useful for test / spec scenarios, and abusing the framework.
240
+ def teleport!( target )
241
+ persister.current_state=( machine.states[target] )
242
+ end
243
+
244
+ # display something sensible that doesn't take up the whole screen
245
+ def inspect
246
+ '<#' + self.class.to_s + ' ' +
247
+ attrs = [[:current_state, state_name.inspect],
248
+ [:object_type , @object.class],
249
+ [:method_name , method_name.inspect],
250
+ [:field_name , field_name.inspect],
251
+ [:machine , machine.to_s]].
252
+ map {|x| x.join('=') }.join( " " ) + '>'
253
+ end
254
+
255
+ # let's be == (and hence ===) the current_state_name as a symbol.
256
+ # a nice little convenience.
257
+ def == other
258
+ if other.respond_to?( :to_sym ) && current_state
259
+ current_state_name == other.to_sym || super( other )
260
+ else
261
+ super( other )
262
+ end
263
+ end
264
+
265
+ # TODO better name
266
+ # is this a binding unique to a specific instance (not bound to a class)?
267
+ def singleton?
268
+ options[:singleton]
269
+ end
270
+
271
+ # SPECME DOCME OR KILLME
272
+ def reload()
273
+ if persister.is_a?( Persistence::ActiveRecord )
274
+ object.reload
275
+ end
276
+ persister.reload
277
+ self
278
+ end
279
+
280
+ def inspect
281
+ s = self.to_s
282
+ s = s[0,s.length-1]
283
+ s << " object=#{object}"
284
+ s << " current_state=#{current_state.to_sym.inspect rescue nil}"
285
+ s << " events=#{events.map(&:to_sym).inspect rescue nil}"
286
+ s << " machine=#{machine.to_s}"
287
+ s << ">"
288
+ s
289
+ end
290
+
291
+ end
292
+ end