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/lib/transition.rb ADDED
@@ -0,0 +1,338 @@
1
+ module StateFu
2
+
3
+ # A 'context' class, created when an event is fired, or needs to be
4
+ # validated.
5
+ #
6
+ # This is what gets yielded to event hooks; it also gets attached
7
+ # to any TransitionHalted exceptions raised.
8
+
9
+ # TODO - make transition evaluate as true if accepted, false if failed, or nil unless fired
10
+
11
+ class Transition
12
+ include Applicable
13
+ include HasOptions
14
+
15
+ attr_reader :binding,
16
+ :machine,
17
+ :origin,
18
+ :target,
19
+ :event,
20
+ :args,
21
+ :errors,
22
+ :object,
23
+ :current_hook_slot,
24
+ :current_hook
25
+
26
+ alias_method :arguments, :args
27
+
28
+ def initialize( binding, event, target=nil, *argument_list, &block )
29
+ # ensure we have an Event
30
+ event = binding.machine.events[event] if event.is_a?(Symbol)
31
+ raise( UnknownTarget.new(self, "Not an event: #{event} #{self.inspect}" )) unless event.is_a? Event
32
+
33
+ @binding = binding
34
+ @machine = binding.machine
35
+ @object = binding.object
36
+ @origin = binding.current_state
37
+
38
+ # ensure we have a target
39
+ target = find_event_target( event, target ) || raise( UnknownTarget.new(self, "target cannot be determined: #{target.inspect} #{self.inspect}"))
40
+
41
+ @target = target
42
+ @event = event
43
+ @errors = []
44
+
45
+ if event.target_for_origin(origin) == target
46
+ # it's a "sequence"
47
+ # which is a hacky way of emulating simpler state machines with
48
+ # state-local events - and in which case, the targets & origins are
49
+ # valid. Quite likely this notion will be removed in time.
50
+ else
51
+ # ensure target is valid for the event
52
+ unless event.targets.include? target
53
+ raise IllegalTransition.new self, "Illegal target #{target} for #{event}"
54
+ end
55
+
56
+ # ensure current_state is a valid origin for the event
57
+ unless event.origins.include? origin
58
+ raise IllegalTransition.new( self, "Illegal event #{event.name} for current state #{binding.state_name}" )
59
+ end
60
+ end
61
+
62
+ machine.inject_helpers_into( self )
63
+ self.args = argument_list
64
+ apply!(argument_list, &block )
65
+ end
66
+
67
+
68
+ def valid?(*args)
69
+ self.args = args unless args.empty?
70
+ requirements_met?(true, true) # revalidate; exit on first failure
71
+ end
72
+
73
+ def args=(args)
74
+ @args = args.extend(TransitionArgsArray).init(self)
75
+ apply!(args) if args.last.is_a?(Hash) unless options.nil?
76
+ end
77
+
78
+ def with(*args)
79
+ self.args = args unless args.empty?
80
+ self
81
+ end
82
+
83
+ def with?(*args)
84
+ valid?
85
+ end
86
+ alias_method :valid_with?, :with?
87
+
88
+ #
89
+ # Requirements
90
+ #
91
+
92
+ def requirements
93
+ origin.exit_requirements + target.entry_requirements + event.requirements
94
+ end
95
+
96
+ def unmet_requirements(revalidate=false, fail_fast=false)
97
+ if revalidate
98
+ @unmet_requirements = nil
99
+ else
100
+ return @unmet_requirements if @unmet_requirements
101
+ end
102
+ result = [requirements].flatten.uniq.inject([]) do |unmet, requirement|
103
+ unmet << requirement unless evaluate(requirement)
104
+ break(unmet) if fail_fast && !unmet.empty?
105
+ unmet
106
+ end
107
+ raise self.inspect if result.nil?
108
+ # don't cache result if it might
109
+ @unmet_requirements = result unless (fail_fast && unmet_requirements.length != 0)
110
+ result
111
+ end
112
+
113
+ def first_unmet_requirement(revalidate=false)
114
+ unmet_requirements(revalidate, fail_fast=true)[0]
115
+ end
116
+
117
+ def unmet_requirement_messages(revalidate=false, fail_fast=false) # TODO
118
+ unmet_requirements(revalidate, fail_fast).map do |requirement|
119
+ evaluate_requirement_message(requirement, revalidate)
120
+ end.extend MessageArray
121
+ end
122
+ alias_method :error_messages, :unmet_requirement_messages
123
+
124
+ # return a hash of requirement_name => evaluated message
125
+ def requirement_errors(revalidate=false, fail_fast=false)
126
+ unmet_requirements(revalidate, fail_fast).
127
+ map { |requirement| [requirement, evaluate_requirement_message(requirement)]}.
128
+ to_h
129
+ end
130
+
131
+ def first_unmet_requirement_message(revalidate=false)
132
+ evaluate_requirement_message(first_unmet_requirement(revalidate), revalidate)
133
+ end
134
+
135
+ # raise a RequirementError unless all requirements are met.
136
+ def check_requirements!(revalidate=false, fail_fast=true) # TODO
137
+ raise RequirementError.new( self, unmet_requirement_messages.inspect ) unless requirements_met?(revalidate, fail_fast)
138
+ end
139
+
140
+ def requirements_met?(revalidate=false, fail_fast=false) # TODO
141
+ unmet_requirements(revalidate, fail_fast).empty?
142
+ end
143
+
144
+ #
145
+ # Hooks
146
+ #
147
+ def hooks_for(element, slot)
148
+ send(element).hooks[slot]
149
+ end
150
+
151
+ def hooks
152
+ StateFu::Hooks::ALL_HOOKS.map do |owner, slot|
153
+ [ [owner, slot], send(owner).hooks[slot] ]
154
+ end
155
+ end
156
+
157
+ def run_hook hook
158
+ evaluate hook
159
+ end
160
+
161
+ #
162
+ # Halt a transition mid-execution
163
+ #
164
+
165
+ # halt a transition with a message
166
+ # can be used to back out of a transition inside eg a state entry hook
167
+ def halt! message
168
+ raise TransitionHalted.new( self, message )
169
+ end
170
+
171
+ #
172
+ # Fire!
173
+ #
174
+
175
+ # actually fire the transition
176
+ def fire!(*arguments) # block?
177
+ raise TransitionAlreadyFired.new(self) if fired?
178
+ self.args = arguments unless arguments.empty?
179
+
180
+ check_requirements!
181
+ @fired = true
182
+ begin
183
+ # duplicated: see #hooks method
184
+ StateFu::Hooks::ALL_HOOKS.map do |owner, slot|
185
+ [ [owner, slot], send(owner).hooks[slot] ]
186
+ end.each do |address, hooks|
187
+ Logger.info("running #{address.inspect} hooks for #{object.class} #{object}")
188
+ owner,slot = *address
189
+ hooks.each do |hook|
190
+ Logger.info("running hook #{hooks} for #{object.class} #{object}")
191
+ @current_hook_slot = address
192
+ @current_hook = hook
193
+ run_hook hook
194
+ end
195
+ if slot == :entry
196
+ @accepted = true
197
+ @binding.persister.current_state = @target
198
+ Logger.info("State is now :#{@target.name} for #{object.class} #{object}")
199
+ end
200
+ end
201
+ # transition complete
202
+ @current_hook_slot = nil
203
+ @current_hook = nil
204
+ rescue TransitionHalted => e
205
+ Logger.info("Transition halted for #{object.class} #{object}: #{e.inspect}")
206
+ @errors << e
207
+ end
208
+ self
209
+ end
210
+
211
+ #
212
+ # It can pretend it's a hash; so the transition makes a good argument to be
213
+ # passed to methods.
214
+ #
215
+ include Enumerable
216
+
217
+ def each *a, &b
218
+ options.each *a, &b
219
+ end
220
+
221
+ def halted?
222
+ !@errors.empty?
223
+ end
224
+
225
+ def fired?
226
+ !!@fired
227
+ end
228
+
229
+ def accepted?
230
+ !!@accepted
231
+ end
232
+ alias_method :complete?, :accepted?
233
+
234
+ def current_state
235
+ binding.current_state
236
+ end
237
+
238
+ # a little convenience for debugging / display
239
+ def destination
240
+ [event, target].map(&:to_sym)
241
+ end
242
+
243
+ #
244
+ # give as many choices as possible
245
+ #
246
+
247
+ alias_method :obj, :object
248
+ alias_method :instance, :object
249
+ alias_method :model, :object
250
+ alias_method :instance, :object
251
+
252
+ alias_method :destination, :target
253
+ alias_method :final_state, :target
254
+ alias_method :to, :target
255
+
256
+ alias_method :original_state, :origin
257
+ alias_method :initial_state, :origin
258
+ alias_method :from, :origin
259
+
260
+ def cycle?
261
+ origin == target
262
+ end
263
+
264
+ # an accepted transition == true
265
+ # an unaccepted transition == false
266
+ # same for === (for case equality)
267
+ def == other
268
+ case other
269
+ when true
270
+ accepted?
271
+ when false
272
+ !accepted?
273
+ when State, Symbol
274
+ current_state == other.to_sym
275
+ when Transition
276
+ inspect == other.inspect
277
+ else
278
+ super( other )
279
+ end
280
+ end
281
+
282
+ # display nice and short
283
+ def inspect
284
+ s = self.to_s
285
+ s = s[0,s.length-1]
286
+ s << " event=#{event.to_sym.inspect}" if event
287
+ s << " origin=#{origin.to_sym.inspect}" if origin
288
+ s << " target=#{target.to_sym.inspect}" if target
289
+ s << " args=#{args.inspect}" if args
290
+ s << " options=#{options.inspect}" if options
291
+ s << ">"
292
+ s
293
+ end
294
+
295
+ private
296
+
297
+ def executioner
298
+ @executioner ||= Executioner.new( self ) do |ex|
299
+ machine.inject_helpers_into( ex )
300
+ machine.inject_methods_into( ex )
301
+ end
302
+ end
303
+
304
+ def evaluate(method_name_or_proc)
305
+ executioner.evaluate(method_name_or_proc)
306
+ end
307
+
308
+ def evaluate_requirement_message( name, revalidate=false)
309
+ @requirement_messages ||= {}
310
+ name = name.to_sym
311
+ return @requirement_messages[name] if @requirement_messages[name] && !revalidate
312
+ msg = machine.requirement_messages[name.to_sym]
313
+ result = case msg
314
+ when String
315
+ msg
316
+ when Symbol, Proc
317
+ evaluate msg
318
+ else
319
+ name
320
+ end
321
+ @requirement_messages[name] = result
322
+ end
323
+
324
+ def find_event_target( evt, tgt )
325
+ case tgt
326
+ when StateFu::State
327
+ tgt
328
+ when Symbol
329
+ binding && binding.machine.states[ tgt ]
330
+ when NilClass
331
+ evt.respond_to?(:target) && evt.target
332
+ else
333
+ raise ArgumentError.new( "#{tgt.class} is not a Symbol, StateFu::State or nil (#{evt})" )
334
+ end
335
+ end
336
+
337
+ end
338
+ end
@@ -0,0 +1,224 @@
1
+ module StateFu
2
+ class TransitionQuery
3
+ attr_accessor :binding, :options, :result, :args, :block
4
+
5
+ def initialize(binding, options={})
6
+ defaults = { :valid => true, :cyclic => nil }
7
+ @options = defaults.merge(options).symbolize_keys
8
+ @binding = binding
9
+ end
10
+
11
+ include Enumerable
12
+
13
+ def each *a, &b
14
+ result.each *a, &b
15
+ end
16
+
17
+ # calling result() will cause the set of transitions to be calculated -
18
+ # the cat will then be either dead or alive; until then it's a litte from
19
+ # column A, a little from column B.
20
+ def method_missing(method_name, *args, &block)
21
+ if result.respond_to?(method_name, true)
22
+ result.__send__(method_name, *args, &block)
23
+ else
24
+ super(method_name, *args, &block)
25
+ end
26
+ end
27
+
28
+ # prepare the query with arguments / block
29
+ # so that they can be applied to the transition once one is selected
30
+ def with(*args, &block)
31
+ @args = args
32
+ @block = block if block_given?
33
+ self
34
+ end
35
+
36
+ # build a list of possible transition destinations ([event, target])
37
+ # without actually constructing any transition objects
38
+ # def all_destinations
39
+ # binding.events.inject([]){ |arr, evt| arr += evt.targets.map{|tgt| [evt,tgt] }; arr}.uniq
40
+ # end
41
+ #
42
+ # def all_destination_names
43
+ # all_destinations.map {|tuple| tuple.map(&:to_sym) }
44
+ # end
45
+
46
+ #
47
+ # Chainable Filters
48
+ #
49
+
50
+ def cyclic
51
+ @options.merge! :cyclic => true
52
+ self
53
+ end
54
+
55
+ def not_cyclic
56
+ @options.merge! :cyclic => false
57
+ self
58
+ end
59
+
60
+ def valid
61
+ @options.merge! :valid => true
62
+ self
63
+ end
64
+
65
+ def not_valid
66
+ @options.merge! :valid => false
67
+ self
68
+ end
69
+ alias_method :invalid, :not_valid
70
+
71
+ def to state
72
+ @options.merge! :target => state
73
+ self
74
+ end
75
+
76
+ def for_event event
77
+ @options.merge! :event => event
78
+ self
79
+ end
80
+
81
+ def simple
82
+ @options.merge! :simple => true
83
+ self
84
+ end
85
+
86
+ #
87
+ # Means to an outcome
88
+ #
89
+
90
+ # find a transition by event and optionally (optional if it can be inferred) target.
91
+ def find(destination=nil, &block)
92
+ # use the prepared event & target, and block, if none are supplied
93
+ event, target = destination.nil? ? [options[:event], options[:target]] : parse_destination(destination)
94
+ block ||= @block
95
+ returning Transition.new(binding, event, target, &block) do |transition|
96
+ if @args
97
+ transition.args = @args
98
+ end
99
+ end
100
+ end
101
+
102
+ def singular
103
+ result.first if result.length == 1
104
+ end
105
+
106
+ def singular?
107
+ !!singular
108
+ end
109
+
110
+ def next
111
+ @options[:cyclic] ||= false
112
+ singular
113
+ end
114
+
115
+ def next_state
116
+ @options[:cyclic] ||= false
117
+ if result.map(&:target).uniq.length == 1
118
+ result.first.target
119
+ end
120
+ end
121
+
122
+ def next_event
123
+ @options[:cyclic] ||= false
124
+ if result.map(&:event).uniq.length == 1
125
+ result.first.event
126
+ end
127
+ end
128
+
129
+ def events
130
+ map {|t| t.event }.extend EventArray
131
+ end
132
+
133
+ def targets
134
+ map {|t| t.target }.extend StateArray
135
+ end
136
+
137
+ private
138
+
139
+ # extend result with this to provide a few conveniences
140
+ module Result
141
+ def states
142
+ map(&:target).uniq.extend StateArray
143
+ end
144
+ alias_method :targets, :states
145
+ alias_method :next_states, :states
146
+
147
+ def events
148
+ map(&:event).uniq.extend EventArray
149
+ end
150
+ end # Result
151
+
152
+ # looks a little complex because of all the places that previously set
153
+ # options can filter the set of transitions - but all it's doing is
154
+ # looping over each event, and each event's possible targets, and building
155
+ # a list of transitions.
156
+
157
+ def result
158
+ @result = binding.events.select do |e|
159
+ case options[:cyclic]
160
+ when true
161
+ e.cycle?
162
+ when false
163
+ !e.cycle?
164
+ else
165
+ true
166
+ end
167
+ end.map do |event|
168
+ next if options[:event] and event != options[:event]
169
+ returning [] do |ts|
170
+
171
+ # TODO hmm ... "sequences" ... undecided on these. see Event / Lathe for more detail
172
+ if options[:sequences]
173
+ if target = event.target_for_origin(current_state)
174
+ ts << binding.transition([event,target], *args) unless options[:cyclic]
175
+ end
176
+ end
177
+
178
+ # build a list of transitions from the possible events and their targets
179
+ if event.targets
180
+ next unless event.target if options[:simple]
181
+ event.targets.flatten.each do |target|
182
+ next if options[:target] and target != options[:target]
183
+ t = Transition.new(binding, event, target, *args)
184
+ ts << t if (t.valid? or !options[:valid])
185
+ end
186
+ end
187
+
188
+ end
189
+ end.flatten.extend(Result)
190
+
191
+ if @args || @block
192
+ @result.each do |t|
193
+ t.apply!( &@block) if @block
194
+ t.args = @args if @args
195
+ end
196
+ end
197
+
198
+ @result
199
+ end # result
200
+
201
+ # sanitizes / extracts destination for find.
202
+ #
203
+ # takes a single, simple (one target only) event,
204
+ # or an array of [event, target],
205
+ # or one of the above with symbols in place of the objects themselves.
206
+ def parse_destination(destination)
207
+ event, target = destination
208
+
209
+ unless event.is_a?(Event)
210
+ event = binding.machine.events[event]
211
+ end
212
+
213
+ unless target.is_a?(State)
214
+ target = binding.machine.states[target] rescue nil
215
+ end
216
+
217
+ raise ArgumentError.new( [event,target].inspect ) unless
218
+ [[Event, State],[Event, NilClass]].include?( [event,target].map(&:class) )
219
+ [event, target]
220
+ end # parse_destination
221
+
222
+ end
223
+ end
224
+