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