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/lib/event.rb ADDED
@@ -0,0 +1,192 @@
1
+ module StateFu
2
+ class Event < StateFu::Sprocket
3
+
4
+ attr_reader :origins, :targets, :requirements, :sequence
5
+
6
+ # called by Lathe when a new event is constructed
7
+ def initialize(machine, name, options={})
8
+ @requirements = [].extend ArrayWithSymbolAccessor
9
+ @sequence = {}
10
+ super( machine, name, options )
11
+ end
12
+
13
+ # Sequences: pretending events are state-local -
14
+ # probably a bad idea but here for a "compatibility mode"
15
+ # with eg the activemodel state machine
16
+
17
+ # build a hash of target => [origins]
18
+ def add_to_sequence origin_states, target_state
19
+ origin_states = [origin_states].flatten
20
+ existing = origin_states.select {|s| target_for_origin(s) }
21
+ raise ArgumentError.new unless existing.empty? && !targets
22
+ @sequence[target_state] ||= []
23
+ [origin_states].flatten.each do |o|
24
+ @sequence[target_state] << o
25
+ end
26
+ @sequence
27
+ end
28
+
29
+ def sequence?
30
+ !sequence.empty?
31
+ end
32
+
33
+ def target_for_origin origin_state
34
+ raise ArgumentError.new if origin_state.nil?
35
+ name = sequence.detect do |k,v|
36
+ v.include?(origin_state.to_sym)
37
+ end[0] rescue nil
38
+ machine.states[name] if name
39
+ end
40
+
41
+
42
+ def can_transition_from?(origin_state)
43
+ ( origins && origins.include?(origin_state.to_sym) && !targets.blank?) ||
44
+ target_for_origin(origin_state)
45
+ end
46
+
47
+ # the names of all possible origin states
48
+ def origin_names
49
+ origins ? origins.map(&:to_sym) : nil
50
+ end
51
+
52
+ # the names of all possible target states
53
+ def target_names
54
+ targets ? targets.map(&:to_sym) : nil
55
+ end
56
+
57
+ # tests if a state or state name is in the list of targets
58
+ def to?( state )
59
+ target_names.include?( state.to_sym )
60
+ end
61
+
62
+ # tests if a state or state name is in the list of origins
63
+ def from?( state )
64
+ origin_names.include?( state.to_sym ) || target_for_origin(state)
65
+ end
66
+
67
+ def cycle?
68
+ origin && (origin == target)
69
+ end
70
+
71
+ # *adds to* the origin states given a list of symbols / States
72
+ def origins=( *args )
73
+ update_state_collection( '@origins', *args )
74
+ end
75
+
76
+ # *adds to* the target states given a list of symbols / States
77
+ def targets=( *args )
78
+ update_state_collection( '@targets', *args )
79
+ end
80
+
81
+ # if there is a single state in #origins, returns it
82
+ def origin
83
+ origins && origins.length == 1 && origins[0] || nil
84
+ end
85
+
86
+ # if there is a single state in #origins, returns it
87
+ def target
88
+ targets && targets.length == 1 && targets[0] || nil
89
+ end
90
+
91
+ # a simple event has exactly one target, and any number of
92
+ # origins. It's simple because it can be triggered without
93
+ # supplying a target name - ie, <tt>go!<tt> vs <tt>go!(:home)<tt>
94
+ def simple?
95
+ !! ( origins && target || sequence? )
96
+ end
97
+
98
+ def fireable?( transition )
99
+ transition.valid?(true)
100
+ end
101
+
102
+
103
+ #
104
+ # Lathe methods
105
+ #
106
+
107
+ # adds an event requirement.
108
+ # DOCME // TODO - can this be removed?
109
+ def requires( *args, &block )
110
+ lathe.requires( *args, &block )
111
+ end
112
+
113
+ # generally called from a Lathe. Sets the origin(s) and optionally
114
+ # target(s) - that is, if you supply the :to option, or a single element
115
+ # hash of origins => targets ) of the event. Both origins= and
116
+ # targets= are accumulators.
117
+ def from *args
118
+ options = args.extract_options!.symbolize_keys!
119
+ args.flatten!
120
+ to = options.delete(:to) || options.delete(:transitions_to)
121
+ if args.empty? && !to
122
+ if options.length == 1
123
+ self.origins = options.keys[0]
124
+ self.targets = options.values[0]
125
+ else
126
+ raise options.inspect
127
+ end
128
+ else
129
+ self.origins = *args
130
+ self.targets = to unless to.nil?
131
+ end
132
+ end
133
+
134
+ # sets the target states for the event.
135
+ def to *args
136
+ options = args.extract_options!.symbolize_keys!
137
+ args.flatten!
138
+ raise options.inspect unless options.empty?
139
+ self.targets= *args
140
+ end
141
+
142
+ alias_method :transitions_to, :to
143
+ alias_method :transitions_from, :from
144
+
145
+ #
146
+ # misc
147
+ #
148
+
149
+ # display nice and short
150
+ def inspect
151
+ s = self.to_s
152
+ s = s[0,s.length-1]
153
+ display_hooks = hooks.dup
154
+ display_hooks.each do |k,v|
155
+ display_hooks.delete(k) if v.empty?
156
+ end
157
+ unless display_hooks.empty?
158
+ s << " hooks=#{display_hooks.inspect}"
159
+ end
160
+ unless requirements.empty?
161
+ s << " requirements=#{requirements.inspect}"
162
+ end
163
+ s << " targets=#{targets.map(&:to_sym).inspect}" if targets
164
+ s << " origins=#{origins.map(&:to_sym).inspect}" if origins
165
+ s << ">"
166
+ s
167
+ end
168
+
169
+ private
170
+
171
+ # internal method which accumulates states into an instance
172
+ # variable with successive invocations.
173
+ # ensures that calling #from multiple times adds to, rather than
174
+ # clobbering, the list of origins / targets.
175
+ def update_state_collection( ivar_name, *args)
176
+ raise ArgumentError if sequence?
177
+ new_states = if [args].flatten == [:ALL]
178
+ machine.states
179
+ else
180
+ machine.find_or_create_states_by_name( *args.flatten )
181
+ end
182
+ unless new_states.is_a?( Array )
183
+ new_states = [new_states]
184
+ end
185
+ existing = instance_variable_get( ivar_name )
186
+ # return existing if new_states.empty?
187
+ new_value = ((existing || [] ) + new_states).flatten.compact.uniq.extend( StateArray )
188
+ instance_variable_set( ivar_name, new_value )
189
+ end
190
+
191
+ end
192
+ end
@@ -0,0 +1,120 @@
1
+ module StateFu
2
+ #
3
+ # delegator class for evaluation methods / procs in the context of
4
+ # your object.
5
+ #
6
+
7
+ class Executioner
8
+
9
+ # give us a blank slate
10
+ # instance_methods.each { |m| undef_method m unless m =~ /(^__|^self|^nil\?$|^send$|proxy_|^object_id|^respond_to\?|^instance_exec|^instance_eval|^method$)/ }
11
+
12
+ def initialize transition, &block
13
+ @transition = transition
14
+ @__target__ = transition.object
15
+ @__self___ = self
16
+ yield self if block_given?
17
+ # forces method_missing to snap back to its pre-state-fu condition:
18
+ # @__target__.initialize_state_fu!
19
+ self
20
+ end
21
+
22
+ delegate :origin, :to => :transition, :prefix => true # transition_origin
23
+ delegate :target, :to => :transition, :prefix => true # transition_target
24
+ delegate :event, :to => :transition, :prefix => true # transition_event
25
+
26
+ delegate :halt!, :to => :transition
27
+ delegate :args, :to => :transition
28
+ delegate :options, :to => :transition
29
+
30
+ def binding
31
+ transition.binding
32
+ end
33
+
34
+ attr_reader :transition, :__target__, :__self__
35
+
36
+ alias_method :t, :transition
37
+ alias_method :current_transition, :transition
38
+ alias_method :context, :transition
39
+ alias_method :ctx, :transition
40
+
41
+ alias_method :arguments, :args
42
+ alias_method :transition_arguments, :args
43
+
44
+ def machine
45
+ binding.machine
46
+ end
47
+
48
+ def states
49
+ machine.states
50
+ end
51
+ # delegate :machine, :to => :transition
52
+
53
+ def evaluate_with_arguments method_name_or_proc, *arguments
54
+ if method_name_or_proc.is_a?(Proc) && meth = method_name_or_proc
55
+ elsif meth = transition.machine.named_procs[method_name_or_proc]
56
+ elsif respond_to?( method_name_or_proc) && meth = method(method_name_or_proc)
57
+ elsif method_name_or_proc.to_s =~ /^not?_(.*)$/
58
+ # special case: prefix a method with no_ or not_ and get the
59
+ # boolean opposite of its evaluation result
60
+ return !( evaluate_with_arguments $1, *args )
61
+ else
62
+ raise NoMethodError.new( "undefined method_name `#{method_name_or_proc.to_s}' for \"#{__target__}\":#{__target__.class.to_s}" )
63
+ end
64
+
65
+ if arguments.length < meth.arity.abs && meth.arity != -1
66
+ # ensure we don't have too few arguments
67
+ raise ArgumentError.new([meth.arity, arguments.length].inspect)
68
+ else
69
+ # ensure we don't pass too many arguments
70
+ arguments = arguments[0, meth.arity.abs]
71
+ end
72
+
73
+ # execute it!
74
+ __target__.with_methods_on(self) do
75
+ self.instance_exec *arguments, &meth
76
+ end
77
+ end
78
+
79
+ def evaluate method_name_or_proc
80
+ arguments = [transition, args, __target__]
81
+ evaluate_with_arguments(method_name_or_proc, *arguments)
82
+ end
83
+
84
+ alias_method :executioner_respond_to?, :respond_to?
85
+
86
+ def respond_to? method_name, include_private = false
87
+ executioner_respond_to?(method_name, include_private) ||
88
+ __target__.__send__( :respond_to?, method_name, include_private )
89
+ end
90
+
91
+ alias_method :executioner_method, :method
92
+ def method method_name
93
+ begin
94
+ executioner_method(method_name)
95
+ rescue NameError
96
+ __target__.__send__ :method, method_name
97
+ end
98
+ end
99
+
100
+ private
101
+
102
+ # Forwards any missing method call to the \target.
103
+ # TODO / NOTE: we don't (can't ?) handle block arguments ...
104
+ def method_missing(method_name, *args)
105
+ if __target__.respond_to?(method_name, true)
106
+ begin
107
+ meth = __target__.__send__ :method, method_name
108
+ rescue NameError
109
+ super
110
+ end
111
+ __target__.instance_exec( *args, &meth)
112
+ else # let's hope it's a named proc
113
+ evaluate_with_arguments(method_name, *args)
114
+ end
115
+
116
+ end
117
+
118
+ # NOTE: const_missing is not handled.
119
+ end
120
+ end
data/lib/hooks.rb ADDED
@@ -0,0 +1,39 @@
1
+ module StateFu
2
+
3
+ # TODO document structure / sequence of hooks elsewhere
4
+
5
+ module Hooks #:nodoc
6
+
7
+ ALL_HOOKS = [[:machine, :before_all], # global before. prepare for any transition
8
+ [:event, :before], # prepare for the event
9
+ [:origin, :exit], # say goodbye!
10
+ [:event, :execute], # do stuff here for the event
11
+ [:target, :entry], # entry point. last chance to halt!
12
+ [:event, :after], # clean up after transition
13
+ [:target, :accepted], # state is changed. Do something about it.
14
+ [:machine, :after_all]] # global after. close up shop.
15
+
16
+ EVENT_HOOKS = ALL_HOOKS.select { |type, name| type == :event }
17
+ STATE_HOOKS = ALL_HOOKS.select { |type, name| [:origin, :target].include?(type) }
18
+ MACHINE_HOOKS = ALL_HOOKS.select { |type, name| type == :machine }
19
+ HOOK_NAMES = ALL_HOOKS.map(&:last)
20
+
21
+ # just turn the above into what each class needs
22
+ # and make it into a nice hash: { :name =>[ hook, ... ], ... }
23
+ def self.for( instance )
24
+ case instance
25
+ when State
26
+ STATE_HOOKS
27
+ when Event
28
+ EVENT_HOOKS
29
+ when Machine
30
+ MACHINE_HOOKS
31
+ when Sprocket
32
+ []
33
+ end.
34
+ map { |type, name| [name, [].extend( OrderedHash )] }.
35
+ to_h.extend( OrderedHash ).freeze
36
+ end
37
+
38
+ end
39
+ end
data/lib/interface.rb ADDED
@@ -0,0 +1,132 @@
1
+ module StateFu
2
+ module Interface
3
+ module SoftAlias
4
+
5
+ # define aliases that won't clobber existing methods -
6
+ # so we can be liberal with them.
7
+ def soft_alias(x)
8
+ aliases = [ x.to_a[0] ].flatten
9
+ original = aliases.shift
10
+ existing_method_names = (self.instance_methods | self.protected_instance_methods | self.private_instance_methods).map(&:to_sym)
11
+ taken, ok = aliases.partition { |a| existing_method_names.include?(a.to_sym) }
12
+ StateFu::Logger.debug("#{self.to_s} alias for ## #{original} already taken: #{taken.inspect}") unless taken.empty?
13
+ ok.each { |a| alias_method a, original}
14
+ end
15
+
16
+ end
17
+
18
+ module Aliases
19
+
20
+ def self.extended(base)
21
+ base.extend SoftAlias
22
+ base.class_eval do
23
+ # instance method aliases
24
+ soft_alias :state_fu => [:stfu, :fu, :stateful, :workflow, :engine, :machine, :context]
25
+ soft_alias :state_fu_bindings => [:bindings, :workflows, :engines, :machines, :contexts]
26
+ soft_alias :state_fu! => [:stfu!, :initialize_machines!, :initialize_state!]
27
+ class << self
28
+ extend SoftAlias
29
+ # class method aliases
30
+ soft_alias :state_fu_machine => [:stfu, :state_fu, :workflow, :stateful, :statefully, :state_machine, :engine]
31
+ soft_alias :state_fu_machines => [:stfus, :state_fus, :workflows, :engines]
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ # Provides access to StateFu to your classes. Plenty of aliases are
38
+ # provided so you can use whatever makes sense to you.
39
+ module ClassMethods
40
+
41
+ # TODO:
42
+ # take option :alias => false (disable aliases) or :alias
43
+ # => :foo (add :foo as class & instance accessor methods)
44
+
45
+ # Given no arguments, return the default machine (:state_fu) for the
46
+ # class, creating it if it did not exist.
47
+ #
48
+ # Given a symbol, return the machine by that name, creating it
49
+ # if it didn't exist, and definining it if a block is passed.
50
+ #
51
+ # Given a block, apply it to a StateFu::Lathe to define a
52
+ # machine, and return it.
53
+ #
54
+ # This can be done multiple times; changes are cumulative.
55
+ #
56
+ # You can have as many machines as you like per class.
57
+ #
58
+ # Klass.machine # the default machine named :om
59
+ # # equivalent to Klass.machine(:om)
60
+ # Klass.machine(:workflow) # another totally separate machine
61
+ #
62
+ # recognised options are:
63
+ # :field_name - specify the field to use for persistence.
64
+ # defaults to {machine_name}_field.
65
+ #
66
+ def state_fu_machine( *args, &block )
67
+ options = args.extract_options!.symbolize_keys!
68
+ name = args[0] || DEFAULT
69
+ StateFu::Machine.for_class( self, name, options, &block )
70
+ end
71
+ alias_method :machine, :state_fu_machine
72
+
73
+ def state_fu_field_names
74
+ @_state_fu_field_names ||= {}
75
+ end
76
+
77
+ def state_fu_machines
78
+ @_state_fu_machines ||= {}
79
+ end
80
+ alias_method :machines, :state_fu_machines
81
+
82
+ end
83
+
84
+ # These methods grant access to StateFu::Binding objects, which
85
+ # are bundles of context encapsulating a StateFu::Machine, an instance
86
+ # of a class, and its current state in the machine.
87
+
88
+ module InstanceMethods
89
+
90
+ def state_fu_bindings
91
+ @_state_fu_bindings ||= {}
92
+ end
93
+
94
+ # A StateFu::Binding comes into being when it is first referenced.
95
+ #
96
+ # This is the accessor method through which an object instance (or developer)
97
+ # can access a StateFu::Machine, the object's current state, the
98
+ # methods which trigger event transitions, etc.
99
+
100
+ def state_fu_binding( name = DEFAULT )
101
+ name = name.to_sym
102
+ if machine = self.class.state_fu_machines[name]
103
+ state_fu_bindings[name] ||= StateFu::Binding.new( machine, self, name )
104
+ else raise ArgumentError.new("No state machine called #{name} for #{self.class} #{self}")
105
+ end
106
+ end
107
+ alias_method :state_fu, :state_fu_binding
108
+
109
+ def current_state( name = DEFAULT )
110
+ state_fu_binding(name).current_state
111
+ end
112
+
113
+ def next!(name = DEFAULT, *args, &block )
114
+ state_fu_binding(name).next! *args, &block
115
+ end
116
+ alias_method :next_state!, :next!
117
+ alias_method :fire_next_transition!, :next!
118
+
119
+ # Instantiate bindings for all machines, which ensures that persistence
120
+ # fields are intialized and event methods defined.
121
+ # It's useful to call this before_create w/
122
+ # ActiveRecord classes, as this will cause the database field
123
+ # to be populated with the default state name.
124
+
125
+ def state_fu!
126
+ MethodFactory.define_singleton_method(self, :initialize_state_fu!) { true }
127
+ self.class.state_fu_machines.keys.map { |n| state_fu_binding( n ) }
128
+ end
129
+
130
+ end # ClassMethods
131
+ end # Interface
132
+ end # StateFu