davidlee-state-fu 0.0.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 (49) hide show
  1. data/LICENSE +40 -0
  2. data/README.textile +174 -0
  3. data/Rakefile +87 -0
  4. data/lib/no_stdout.rb +32 -0
  5. data/lib/state-fu.rb +93 -0
  6. data/lib/state_fu/binding.rb +262 -0
  7. data/lib/state_fu/core_ext.rb +23 -0
  8. data/lib/state_fu/event.rb +98 -0
  9. data/lib/state_fu/exceptions.rb +42 -0
  10. data/lib/state_fu/fu_space.rb +50 -0
  11. data/lib/state_fu/helper.rb +189 -0
  12. data/lib/state_fu/hooks.rb +28 -0
  13. data/lib/state_fu/interface.rb +139 -0
  14. data/lib/state_fu/lathe.rb +247 -0
  15. data/lib/state_fu/logger.rb +10 -0
  16. data/lib/state_fu/machine.rb +159 -0
  17. data/lib/state_fu/method_factory.rb +95 -0
  18. data/lib/state_fu/persistence/active_record.rb +27 -0
  19. data/lib/state_fu/persistence/attribute.rb +46 -0
  20. data/lib/state_fu/persistence/base.rb +98 -0
  21. data/lib/state_fu/persistence/session.rb +7 -0
  22. data/lib/state_fu/persistence.rb +50 -0
  23. data/lib/state_fu/sprocket.rb +27 -0
  24. data/lib/state_fu/state.rb +45 -0
  25. data/lib/state_fu/transition.rb +213 -0
  26. data/spec/helper.rb +86 -0
  27. data/spec/integration/active_record_persistence_spec.rb +189 -0
  28. data/spec/integration/class_accessor_spec.rb +127 -0
  29. data/spec/integration/event_definition_spec.rb +74 -0
  30. data/spec/integration/ex_machine_for_accounts_spec.rb +79 -0
  31. data/spec/integration/example_01_document_spec.rb +127 -0
  32. data/spec/integration/example_02_string_spec.rb +87 -0
  33. data/spec/integration/instance_accessor_spec.rb +100 -0
  34. data/spec/integration/machine_duplication_spec.rb +95 -0
  35. data/spec/integration/requirement_reflection_spec.rb +201 -0
  36. data/spec/integration/sanity_spec.rb +31 -0
  37. data/spec/integration/state_definition_spec.rb +177 -0
  38. data/spec/integration/transition_spec.rb +1060 -0
  39. data/spec/spec.opts +7 -0
  40. data/spec/units/binding_spec.rb +145 -0
  41. data/spec/units/event_spec.rb +232 -0
  42. data/spec/units/exceptions_spec.rb +75 -0
  43. data/spec/units/fu_space_spec.rb +95 -0
  44. data/spec/units/lathe_spec.rb +567 -0
  45. data/spec/units/machine_spec.rb +237 -0
  46. data/spec/units/method_factory_spec.rb +359 -0
  47. data/spec/units/sprocket_spec.rb +71 -0
  48. data/spec/units/state_spec.rb +50 -0
  49. metadata +122 -0
@@ -0,0 +1,23 @@
1
+ require 'rubygems'
2
+
3
+ # unless Object.const_defined?('ActiveSupport')
4
+ #
5
+ # require 'active_support/core_ext/array'
6
+ # require 'active_support/core_ext/blank'
7
+ # require 'active_support/core_ext/class'
8
+ # require 'active_support/core_ext/module'
9
+ # require 'active_support/core_ext/hash/keys'
10
+ #
11
+ # class Hash #:nodoc:
12
+ # include ActiveSupport::CoreExtensions::Hash::Keys
13
+ # end
14
+ # end
15
+
16
+ class Symbol
17
+ unless instance_methods.include?(:'<=>')
18
+ # Logger.log ..
19
+ def <=> other
20
+ self.to_s <=> other.to_s
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,98 @@
1
+ module StateFu
2
+ class Event < StateFu::Sprocket
3
+
4
+ attr_reader :origins, :targets, :requirements
5
+
6
+ #
7
+ # TODO - event guards
8
+ #
9
+
10
+ def initialize(machine, name, options={})
11
+ @requirements = [].extend ArrayWithSymbolAccessor
12
+ super( machine, name, options )
13
+ end
14
+
15
+ def origin_names
16
+ origins ? origins.map(&:to_sym) : nil
17
+ end
18
+
19
+ def target_names
20
+ targets ? targets.map(&:to_sym) : nil
21
+ end
22
+
23
+ def to?( state )
24
+ target_names.include?( state.to_sym )
25
+ end
26
+
27
+ def from?( state )
28
+ origin_names.include?( state.to_sym )
29
+ end
30
+
31
+ def origins=( *args )
32
+ if [args].flatten == [:ALL]
33
+ @origins = machine.states
34
+ else
35
+ @origins = machine.find_or_create_states_by_name( *args.flatten ) #.extend( StateArray )
36
+ end
37
+ end
38
+
39
+ def targets=( *args )
40
+ if [args].flatten == [:ALL]
41
+ @targets = machine.states
42
+ else
43
+ @targets = machine.find_or_create_states_by_name( *args.flatten ) # .extend( StateArray )
44
+ end
45
+ end
46
+
47
+ # complete?(:origins) # do we have an origins?
48
+ # complete? # do we have an origins and targets?
49
+ def complete?( field = nil )
50
+ ( field && [field] || [:origins, :targets] ).
51
+ map{ |s| send(s) }.
52
+ all?{ |f| !(f.nil? || f.empty?) }
53
+ end
54
+
55
+ def origin
56
+ origins && origins.length == 1 && origins[0] || nil
57
+ end
58
+
59
+ def target
60
+ targets && targets.length == 1 && targets[0] || nil
61
+ end
62
+
63
+ def simple?
64
+ !! ( origins && target )
65
+ end
66
+
67
+ def from *args
68
+ options = args.extract_options!.symbolize_keys!
69
+ args.flatten!
70
+ to = options.delete(:to)
71
+ if args.empty? && !to
72
+ if options.length == 1
73
+ self.origins= options.keys[0]
74
+ self.targets= options.values[0]
75
+ else
76
+ raise options.inspect
77
+ end
78
+ else
79
+ self.origins= *args
80
+ self.targets= to unless to.nil?
81
+ end
82
+ end
83
+
84
+ def to *args
85
+ options = args.extract_options!.symbolize_keys!
86
+ args.flatten!
87
+ raise options.inspect unless options.empty?
88
+ self.targets= *args
89
+ end
90
+
91
+ def fireable_by?( binding )
92
+ requirements.reject do |r|
93
+ binding.evaluate_requirement( r )
94
+ end.empty?
95
+ end
96
+
97
+ end
98
+ end
@@ -0,0 +1,42 @@
1
+ module StateFu
2
+
3
+ class Exception < ::Exception
4
+ attr_reader :binding, :options
5
+ end
6
+
7
+ class RequirementError < Exception
8
+ end
9
+
10
+ class TransitionHalted < Exception
11
+ attr_reader :transition
12
+
13
+ DEFAULT_MESSAGE = "The transition was halted"
14
+
15
+ def initialize( transition, message=DEFAULT_MESSAGE, options={})
16
+ @transition = transition
17
+ @options = options
18
+ super( message )
19
+ end
20
+ end
21
+
22
+ class InvalidTransition < Exception
23
+ attr_reader :binding, :origin, :target, :event, :args
24
+
25
+ DEFAULT_MESSAGE = "An invalid transition was attempted"
26
+
27
+ def initialize( binding,
28
+ event,
29
+ origin,
30
+ target,
31
+ message=DEFAULT_MESSAGE,
32
+ options={})
33
+ @binding = binding
34
+ @event = event
35
+ @origin = origin
36
+ @target = target
37
+ @options = options
38
+ super( message )
39
+ end
40
+ end
41
+
42
+ end
@@ -0,0 +1,50 @@
1
+ module StateFu
2
+ # Provides a place to stash references.
3
+ # In most cases you won't need to access it directly, though
4
+ # calling reset! before each of your tests/specs can be helpful.
5
+ class FuSpace
6
+ cattr_reader :named_machines, :class_machines, :field_names
7
+
8
+ # class_machines[ Class ][ method_name ] # => a StateFu::Machine
9
+ # class_machines[ Klass ][ nil ] # => the Klass's default Machine
10
+ # field_names[ Class ][ method_name ] # => name of attribute / db field
11
+
12
+ # return the default machine, or an empty hash, given a missing index.
13
+ LAZY_HASH = lambda do |h, k|
14
+ if k.nil?
15
+ self[ StateFu::DEFAULT_MACHINE ]
16
+ else
17
+ h[k]= Hash.new()
18
+ end
19
+ end
20
+
21
+ # Add a machine to StateFu::FuSpace and register it with a given class, by a given name.
22
+ def self.insert!( klass, machine, name, field_name )
23
+ name = name.to_sym
24
+ field_name = field_name.to_sym
25
+ existing_machine = @@class_machines[klass][name]
26
+ if existing_machine && !existing_machine.empty?
27
+ raise("#{klass} already knows a non-empty Machine #{machine} by the name #{name}.")
28
+ else
29
+ @@class_machines[klass][name] = machine
30
+ @@field_names[klass][name] = field_name
31
+ end
32
+ end
33
+ class << self
34
+ alias_method :insert, :insert!
35
+ end
36
+
37
+ # Clears all machines and their bindings to classes.
38
+ # Also initializes the hashes we use to store our references.
39
+ def self.beginners_mind!
40
+ @@named_machines = Hash.new
41
+ @@class_machines = Hash.new( &LAZY_HASH )
42
+ @@field_names = Hash.new( &LAZY_HASH )
43
+ end
44
+ class << self
45
+ alias_method :reset!, :beginners_mind!
46
+ alias_method :forget!, :beginners_mind!
47
+ end
48
+ beginners_mind!
49
+ end
50
+ end
@@ -0,0 +1,189 @@
1
+ module StateFu
2
+
3
+ # Utilities and snippets
4
+ module Helper
5
+
6
+ # Instance methods mixed in on inclusion of StateFu::Helper
7
+ module InstanceMethods
8
+
9
+ # if given a hash of options (or a splatted arglist containing
10
+ # one), merge them into @options. If given a block, eval it
11
+ # (yielding self if the block expects it)
12
+ def apply!( options={}, &block )
13
+ options.respond_to?(:keys) || options = options.extract_options!
14
+ @options.merge!( options.symbolize_keys! )
15
+ return self unless block_given?
16
+ case block.arity
17
+ when 1 # lambda{ |state| ... }
18
+ yield self
19
+ when -1, 0 # lambda{ } ( -1 in ruby 1.8.x but 0 in 1.9.x )
20
+ instance_eval &block
21
+ else
22
+ raise ArgumentError, "unexpected block arity: #{block.arity}"
23
+ end
24
+ self
25
+ end
26
+ alias_method :update!, :apply!
27
+
28
+ end
29
+
30
+ # Class methods mixed in on inclusion of StateFu::Helper
31
+ module ClassMethods
32
+ end
33
+
34
+ def self.included( mod )
35
+ mod.send( :include, InstanceMethods )
36
+ mod.extend( ClassMethods )
37
+ end
38
+
39
+ end
40
+
41
+ # Stuff shared between StateArray and EventArray
42
+ module ArrayWithSymbolAccessor
43
+ # Pass a symbol to the array and get the object with that .name
44
+ # [<Foo @name=:bob>][:bob]
45
+ # => <Foo @name=:bob>
46
+ def []( idx )
47
+ begin
48
+ super( idx )
49
+ rescue TypeError => e
50
+ if idx.respond_to?(:to_sym)
51
+ self.detect { |i| i == idx || i.respond_to?(:name) && i.name == idx.to_sym }
52
+ else
53
+ raise e
54
+ end
55
+ end
56
+ end
57
+
58
+ # so we can go Machine.states.names
59
+ # mildly helpful with irb + readline
60
+ def names
61
+ map(&:name)
62
+ end
63
+
64
+ # SPECME
65
+ def except *syms
66
+ reject {|el| syms.flatten.compact.map(&:to_sym).include?(el.to_sym) } #.extend ArrayWithSymbolAccessor
67
+ end
68
+
69
+ def only *syms
70
+ select {|el| syms.flatten.compact.map(&:to_sym).include?(el.to_sym) } #.extend ArrayWithSymbolAccessor
71
+ end
72
+
73
+ end
74
+
75
+ # Array extender. Used by Machine to keep a list of states.
76
+ module StateArray
77
+ include ArrayWithSymbolAccessor
78
+
79
+ # is there exactly one possible event to fire, with a single
80
+ # target event?
81
+ def next?
82
+ end
83
+
84
+ # if next?, return the state
85
+ def next
86
+ end
87
+
88
+ end
89
+
90
+ # Array extender. Used by Machine to keep a list of events.
91
+ module EventArray
92
+ include ArrayWithSymbolAccessor
93
+
94
+ # return all events transitioning from the given state
95
+ def from( origin )
96
+ select { |e| e.respond_to?(:from?) && e.from?( origin ) }
97
+ end
98
+
99
+ # return all events transitioning to the given state
100
+ def to( target )
101
+ select { |e| e.respond_to?(:to?) && e.to?( target ) }
102
+ end
103
+
104
+ # is there exactly one possible event to fire, with a single
105
+ # target event?
106
+ def next?
107
+ end
108
+
109
+ # if next?, return the event
110
+ def next
111
+ end
112
+
113
+ end
114
+
115
+ # Array extender. Used by Machine to keep a list of helpers to mix into
116
+ # context objects.
117
+ module HelperArray
118
+
119
+ end
120
+
121
+ # Extend an Array with this. It's a fairly compact implementation,
122
+ # though it won't be super fast with lots of elements.
123
+ # items. Internally objects are stored as a list of
124
+ # [:key, 'value'] pairs.
125
+ module OrderedHash
126
+ # if given a symbol / string, treat it as a key
127
+ def []( index )
128
+ begin
129
+ super( index )
130
+ rescue TypeError
131
+ ( x = self.detect { |i| i.first == index }) && x[1]
132
+ end
133
+ end
134
+
135
+ # hash-style setter
136
+ def []=( index, value )
137
+ begin
138
+ super( index, value )
139
+ rescue TypeError
140
+ ( x = self.detect { |i| i.first == index }) ?
141
+ x[1] = value : self << [ index, value ].extend( OrderedHash )
142
+ end
143
+ end
144
+
145
+ # poor man's Hash.keys
146
+ def keys
147
+ map(&:first)
148
+ end
149
+
150
+ # poor man's Hash.values
151
+ def values
152
+ map(&:last)
153
+ end
154
+ end # OrderedHash
155
+
156
+ module ContextualEval
157
+ module InstanceMethods
158
+ def evaluate( &proc )
159
+ if proc.arity == 1
160
+ object.instance_exec( self, &proc )
161
+ else
162
+ instance_eval( &proc )
163
+ end
164
+ end
165
+
166
+ def call_on_object_with_self( name )
167
+ # call a normal method on the object
168
+ # passing the transition as the argument if expected
169
+ if object.method(name).arity == 1
170
+ object.send( name, self )
171
+ else
172
+ object.send( name )
173
+ end
174
+ end
175
+
176
+ def evaluate_named_proc_or_method( name )
177
+ if (name.is_a?( Proc ) && proc = name) || proc = machine.named_procs[ name ]
178
+ evaluate &proc
179
+ else
180
+ call_on_object_with_self( name )
181
+ end
182
+ end
183
+ end
184
+
185
+ def self.included( klass )
186
+ klass.send :include, InstanceMethods
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,28 @@
1
+ module StateFu
2
+ module Hooks
3
+
4
+ ALL_HOOKS = [[:event, :before], # good place to start a transaction, etc
5
+ [:origin, :exit], # say goodbye!
6
+ [:event, :execute], # do stuff here, as a rule of thumb
7
+ [:target, :entry], # last chance to halt!
8
+ [:event, :after], # clean up all the mess
9
+ [:target, :accepted]] # state changed. Quicksave!
10
+
11
+ EVENT_HOOKS = ALL_HOOKS.select { |type, name| type == :event }
12
+ STATE_HOOKS = ALL_HOOKS - EVENT_HOOKS
13
+ HOOK_NAMES = ALL_HOOKS.map {|a| a[1] }
14
+
15
+ # just turn the above into what each class needs
16
+ # and make it into a nice hash: { :name =>[ hook, ... ], ... }
17
+ def self.for( me )
18
+ x = if me.is_a?( StateFu::State ); STATE_HOOKS
19
+ elsif me.is_a?( StateFu::Event ); EVENT_HOOKS
20
+ else {}
21
+ end.
22
+ map { |_,name| [name, [].extend( StateFu::OrderedHash )] }
23
+ hash = x.inject({}) {|h, a| h[a[0]] = a[1] ; h}
24
+ hash.extend( StateFu::OrderedHash ).freeze
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,139 @@
1
+ module StateFu
2
+ module Interface
3
+ # Provides access to StateFu to your classes. Plenty of aliases are
4
+ # provided so you can use whatever makes sense to you.
5
+ module ClassMethods
6
+
7
+ # TODO:
8
+ # take option :alias => false (disable aliases) or :alias
9
+ # => :foo (use foo as class & instance accessor)
10
+
11
+ #
12
+ # Given no arguments, return the default machine (:state_fu) for the
13
+ # class, creating it if it did not exist.
14
+ #
15
+ # Given a symbol, return the machine by that name, creating it
16
+ # if it didn't exist.
17
+ #
18
+ # Given a block, also define it with the contents of the block.
19
+ #
20
+ # This can be done multiple times; changes are cumulative.
21
+ #
22
+ # You can have as many machines as you like per class.
23
+ #
24
+ # Klass.machine # the default machine named :om
25
+ # # equivalent to Klass.machine(:om)
26
+ # Klass.machine(:workflow) # another totally separate machine
27
+ #
28
+ # machine( name=:state_fu, options[:field_name], &block )
29
+
30
+ def machine( *args, &block )
31
+ options = args.extract_options!.symbolize_keys!
32
+ name = args[0] || StateFu::DEFAULT_MACHINE
33
+ StateFu::Machine.for_class( self, name, options, &block )
34
+ end
35
+ alias_method :stfu, :machine
36
+ alias_method :state_fu, :machine
37
+ alias_method :workflow, :machine
38
+ alias_method :statefully, :machine
39
+ alias_method :state_machine, :machine
40
+ alias_method :stateful, :machine
41
+ alias_method :workflow, :machine
42
+ alias_method :engine, :machine
43
+
44
+ # return a hash of :name => StateFu::Machine for your class.
45
+ def machines( *args, &block )
46
+ if args.empty? && !block_given?
47
+ StateFu::FuSpace.class_machines[self]
48
+ else
49
+ machine( *args, &block)
50
+ end
51
+ end
52
+ alias_method :machines, :machines
53
+ alias_method :workflows, :machines
54
+ alias_method :engines, :machines
55
+
56
+ # return the list of machines names for this class
57
+ def machine_names()
58
+ StateFu::FuSpace.class_machines[self].keys
59
+ end
60
+ alias_method :machine_names, :machine_names
61
+ alias_method :workflow_names, :machine_names
62
+ alias_method :engine_names, :machine_names
63
+ end
64
+
65
+ # Give the gift of state to your objects. These methods
66
+ # grant access to StateFu::Binding objects, which are bundles of
67
+ # context linking a StateFu::Machine to an object / instance.
68
+ # Again, plenty of aliases are provided so you can use whatever
69
+ # makes sense to you.
70
+ module InstanceMethods
71
+ private
72
+ def _state_fu
73
+ @_state_fu ||= {}
74
+ end
75
+
76
+ # A StateFu::Binding comes into being, linking your object and a
77
+ # machine, when you first call yourobject.binding() for that
78
+ # machine.
79
+ #
80
+ # Like the class method .machine(), calling it without any arguments
81
+ # is equivalent to passing :om.
82
+ #
83
+ # Essentially, this is the accessor method through which an instance
84
+ # can see and change its state, interact with events, etc.
85
+ #
86
+ public
87
+ def _binding( name=StateFu::DEFAULT_MACHINE )
88
+ name = name.to_sym
89
+ if mach = StateFu::FuSpace.class_machines[self.class][name]
90
+ _state_fu[name] ||= StateFu::Binding.new( mach, self, name )
91
+ end
92
+ end
93
+
94
+ alias_method :fu, :_binding
95
+ alias_method :stfu, :_binding
96
+ alias_method :state_fu, :_binding
97
+ alias_method :stateful, :_binding
98
+ alias_method :workflow, :_binding
99
+ alias_method :engine, :_binding
100
+ alias_method :context, :_binding
101
+
102
+
103
+ # Gain awareness of all bindings (state contexts) this object
104
+ # has contemplated into being.
105
+ # Returns a Hash of { :name => <StateFu::Binding>, ... }
106
+ def _bindings()
107
+ _state_fu
108
+ end
109
+
110
+ alias_method :fus, :_bindings
111
+ alias_method :stfus, :_bindings
112
+ alias_method :state_fus, :_bindings
113
+ alias_method :state_foos, :_bindings
114
+ alias_method :workflows, :_bindings
115
+ alias_method :engines, :_bindings
116
+ alias_method :bindings, :_bindings
117
+ alias_method :machines, :_bindings # not strictly accurate, but makes sense sometimes
118
+ alias_method :contexts, :_bindings
119
+
120
+ # Instantiate bindings for all machines defined for this class.
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
+ def state_fu!( *names )
125
+ if [names || [] ].flatten!.map! {|n| n.to_sym }.empty?
126
+ names = self.class.machine_names()
127
+ end
128
+ @state_fu_initialized = true
129
+ names.map { |n| _binding( n ) }
130
+ end
131
+ alias_method :fu!, :state_fu!
132
+ alias_method :stfu!, :state_fu!
133
+ alias_method :state_fu!, :state_fu!
134
+ alias_method :init_machines!, :state_fu!
135
+ alias_method :initialize_state!, :state_fu!
136
+ alias_method :build_workflow!, :state_fu!
137
+ end
138
+ end
139
+ end