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