state_manager 0.2.3

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.
@@ -0,0 +1,169 @@
1
+ module StateManager
2
+
3
+ class StateNotFound < StandardError; end;
4
+ class InvalidEvent < StandardError; end;
5
+ class InvalidTransition < StandardError; end;
6
+
7
+ # The base StateManager class is responsible for tracking the current state
8
+ # of an object as well as managing the transitions between states.
9
+ class Base < State
10
+
11
+ class_attribute :_resource_class
12
+ class_attribute :_resource_name
13
+ class_attribute :_state_property
14
+ self._state_property = :state
15
+
16
+ attr_accessor :resource, :context
17
+
18
+ def initialize(resource, context={})
19
+ super(nil, nil)
20
+ self.resource = resource
21
+ self.context = context
22
+
23
+ transition_to(initial_state.path) unless current_state
24
+ end
25
+
26
+ # Transitions to the state at the specified path. The path can be relative
27
+ # to any state along the current state's path.
28
+ def transition_to(path)
29
+ path = path.to_s
30
+ state = current_state || self
31
+ exit_states = []
32
+
33
+ # Find the nearest parent state on the path of the current state which
34
+ # has a sub-state at the given path
35
+ new_states = state.find_states(path)
36
+ while(!new_states) do
37
+ exit_states << state
38
+ state = state.parent_state
39
+ raise(StateNotFound, path) unless state
40
+ new_states = state.find_states(path)
41
+ end
42
+
43
+ # Can only transition to leaf states
44
+ # TODO: transition to the initial_state of the state?
45
+ raise(InvalidTransition, path) unless new_states.last.leaf?
46
+
47
+ enter_states = new_states - exit_states
48
+ exit_states = exit_states - new_states
49
+
50
+ from_state = current_state
51
+ to_state = enter_states.last
52
+
53
+ # Before Callbacks
54
+ will_transition(from_state, to_state, current_event)
55
+ exit_states.each{ |s| s.exit }
56
+ enter_states.each{ |s| s.enter }
57
+
58
+ # Set the state on the underlying resource
59
+ self.current_state = to_state
60
+
61
+ # After Callbacks
62
+ exit_states.each{ |s| s.exited }
63
+ enter_states.each{ |s| s.entered }
64
+ did_transition(from_state, to_state, current_event)
65
+ end
66
+
67
+ def current_state
68
+ path = read_state
69
+ find_state(path) if path && !path.empty?
70
+ end
71
+
72
+ def current_state=(value)
73
+ write_state(value)
74
+ end
75
+
76
+ # Send an event to the current state.
77
+ #
78
+ # Unlike the regular send_event method, this method recursively walks the
79
+ # path of states starting at the current state.
80
+ def send_event!(name, *args)
81
+ self.current_event = name
82
+ state = find_state_for_event(name)
83
+ raise(InvalidEvent, name) unless state
84
+ state.send_event name, *args
85
+ self.current_event = nil
86
+ end
87
+
88
+ def respond_to_event?(name)
89
+ !!find_state_for_event(name)
90
+ end
91
+
92
+ def find_state_for_event(name)
93
+ state = current_state
94
+ while(state) do
95
+ return state if state.has_event?(name)
96
+ state = state.parent_state
97
+ end
98
+ end
99
+
100
+ def state_manager
101
+ self
102
+ end
103
+
104
+ def to_s
105
+ "#{current_state.path}" if current_state
106
+ end
107
+
108
+ # Returns true if the underlying object is in the state specified by the
109
+ # given path. An object is 'in' a state if the state lies at any point of
110
+ # the current state's path. E.g:
111
+ #
112
+ # state_manager.current_state.path # returns 'outer.inner'
113
+ # state_manager.in_state? 'outer' # true
114
+ # state_manager.in_state? 'outer.inner' # true
115
+ # state_manager.in_state? 'inner' # false
116
+ #
117
+ def in_state?(path)
118
+ self.find_states(current_state.path).include? find_state(path)
119
+ end
120
+
121
+ # These methods can be overriden by an adapter
122
+ def write_state(value)
123
+ resource.send "#{self.class._state_property.to_s}=", value.path
124
+ end
125
+
126
+ def read_state
127
+ resource.send self.class._state_property
128
+ end
129
+
130
+ def will_transition(from, to, event)
131
+ end
132
+
133
+ def did_transition(from, to, event)
134
+ end
135
+
136
+ # All events the current state will respond to
137
+ def available_events
138
+ state = current_state
139
+ ret = {}
140
+ while(state) do
141
+ ret = state.class.specification.events.merge(ret)
142
+ state = state.parent_state
143
+ end
144
+ ret
145
+ end
146
+
147
+ def self.infer_resource_name!
148
+ return if _resource_name
149
+ if name =~ /States/
150
+ self._resource_name = name.demodulize.gsub(/States/, '').underscore
151
+ create_resource_accessor!(_resource_name)
152
+ end
153
+ end
154
+
155
+ def self.inherited(base)
156
+ super(base)
157
+ base.infer_resource_name!
158
+ end
159
+
160
+ def self.added_to_resource(klass, property, options)
161
+ end
162
+
163
+ protected
164
+
165
+ attr_accessor :current_event
166
+
167
+ end
168
+
169
+ end
@@ -0,0 +1,7 @@
1
+ require 'state_manager/state'
2
+ require 'state_manager/base'
3
+ require 'state_manager/helpers'
4
+ require 'state_manager/dsl'
5
+ require 'state_manager/resource'
6
+ require 'state_manager/adapters'
7
+ require 'state_manager/plugins'
@@ -0,0 +1,77 @@
1
+ require 'active_support/core_ext'
2
+
3
+ module StateManager
4
+ module DSL
5
+
6
+ module State
7
+ # Specifies a state that is a child of the current state
8
+ def state(name, klass=nil, &block)
9
+ # If no base class is specified we look for a class inside the current
10
+ # state's class which has the same name as the state
11
+ const_name = name.capitalize
12
+ klass ||= if const_defined?(const_name)
13
+ self.const_get(name.capitalize)
14
+ else
15
+ Class.new(StateManager::State)
16
+ end
17
+ klass = Class.new(klass, &block) if block_given?
18
+
19
+ remove_const const_name if const_defined?(const_name)
20
+ const_set(const_name, klass)
21
+
22
+ specification.states[name.to_sym] = klass
23
+ end
24
+
25
+ # Specifies an event on the current state
26
+ def event(name, options={}, &block)
27
+ name = name.to_sym
28
+ event = options.dup
29
+ event[:name] = name
30
+ specification.events[name] = event
31
+ define_method name, &block if block_given?
32
+ end
33
+
34
+ # Helper to simplify creating dsl reader methods for specification
35
+ # properties
36
+ module_eval do
37
+ def self.spec_property(name)
38
+ class_eval do
39
+ define_method name do |value|
40
+ specification.send "#{name}=", value
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ # The initial state
47
+ def initial_state(value)
48
+ specification.initial_state = value
49
+ end
50
+ end
51
+
52
+ module Base
53
+ def resource_class(value)
54
+ self._resource_class = value
55
+ end
56
+
57
+ def resource_name(value)
58
+ self._resource_name = value
59
+ create_resource_accessor!(_resource_name)
60
+ end
61
+
62
+ def state_property(value)
63
+ self._state_property = value
64
+ end
65
+ end
66
+
67
+ end
68
+
69
+ class State
70
+ extend DSL::State
71
+ end
72
+
73
+ class Base
74
+ extend DSL::Base
75
+ end
76
+
77
+ end
@@ -0,0 +1,47 @@
1
+ module StateManager
2
+ # State helper methods. Examples:
3
+ #
4
+ # @post.event! # send_event! :event
5
+ # @post.active? # in_state? :active
6
+ # @post.can_event? # respond_to_event? :event
7
+ #
8
+ module Helpers
9
+
10
+ module Methods
11
+ def self.define_methods(specification, target_class, property)
12
+ self.define_methods_helper(specification, target_class, [], property)
13
+ end
14
+
15
+ def self.define_methods_helper(specification, target_class, name_parts, property)
16
+ sm_proc = Proc.new do
17
+ self.send "#{property}_manager"
18
+ end
19
+
20
+ specification.events.each do |name, event|
21
+ target_class.send :define_method, "#{name.to_s}!" do | *args |
22
+ state_manager = instance_eval &sm_proc
23
+ state_manager.send_event! name, *args
24
+ end
25
+
26
+ target_class.send :define_method, "can_#{name.to_s}?" do
27
+ state_manager = instance_eval &sm_proc
28
+ state_manager.respond_to_event?(name)
29
+ end
30
+ end
31
+
32
+ specification.states.each do |name, child_class|
33
+ state_name_parts = name_parts.dup << name
34
+ method = state_name_parts.join('_')
35
+ path = state_name_parts.join('.')
36
+ target_class.send :define_method, "#{method}?" do
37
+ state_manager = instance_eval &sm_proc
38
+ state_manager.in_state?(path)
39
+ end
40
+
41
+ define_methods_helper(child_class.specification, target_class, state_name_parts, property)
42
+ end
43
+ end
44
+ end
45
+
46
+ end
47
+ end
@@ -0,0 +1,54 @@
1
+ if defined?(Delayed)
2
+
3
+ module StateManager
4
+ # Adds support for a :delay property on event definitions. Events with a
5
+ # delay set will be automatically sent after the delay. If the state is
6
+ # changed such that the event is no longer available before the delay is
7
+ # reached, it will be canceled.
8
+ module DelayedJob
9
+
10
+ class DelayedEvent < Struct.new(:path, :event, :state_manager)
11
+ def perform
12
+ return unless state_manager.respond_to_event?(event[:name]) &&
13
+ state_manager.in_state?(path)
14
+ state_manager.send_event! event[:name]
15
+ end
16
+ end
17
+
18
+ module State
19
+
20
+ def delayed_events
21
+ self.class.specification.events.reject{|name,event|!event[:delay]}
22
+ end
23
+
24
+ def entered
25
+ delayed_events.each do |name, event|
26
+ delay = event[:delay]
27
+ delayed_event = DelayedEvent.new(path, event, state_manager)
28
+ Delayed::Job.enqueue delayed_event, :run_at => delay.from_now
29
+ end
30
+ end
31
+
32
+ def exited
33
+ # TODO: we currently just have logic inside the job itself which
34
+ # skips the event if it is no longer relevant. This is not perfect.
35
+ # Ideally we should cancel events in this method (requiring an
36
+ # efficient way to do this without looping over all events).
37
+ end
38
+ end
39
+ end
40
+
41
+ class State
42
+ include DelayedJob::State
43
+
44
+ def entered
45
+ super
46
+ end
47
+
48
+ def exited
49
+ super
50
+ end
51
+ end
52
+ end
53
+
54
+ end
@@ -0,0 +1,4 @@
1
+ # Load each available plugin
2
+ Dir["#{File.dirname(__FILE__)}/plugins/*.rb"].sort.each do |path|
3
+ require "state_manager/plugins/#{File.basename(path)}"
4
+ end
@@ -0,0 +1,83 @@
1
+ module StateManager
2
+ module Resource
3
+
4
+ def self.extended(base)
5
+ base.instance_eval do
6
+ class_attribute :state_managers
7
+ self.state_managers = {}
8
+
9
+ attr_accessor :state_managers
10
+ end
11
+
12
+ base.send :include, InstanceMethods
13
+ end
14
+
15
+ def state_manager(property=:state, klass=nil, options={}, &block)
16
+ default_options = {:helpers => true}
17
+ options = default_options.merge(options)
18
+
19
+ klass ||= begin
20
+ "#{self.name}States".constantize
21
+ rescue NameError
22
+ nil
23
+ end
24
+ klass ||= StateManager::Base
25
+
26
+ # Create a subclass of the specified state manager and mixin an adapter
27
+ # if a matching one is found
28
+ this = self
29
+ adapter = Adapters.match(self)
30
+ resource_name = self.name.demodulize.underscore
31
+
32
+ klass = Class.new(klass) do
33
+ state_property property
34
+ resource_class this
35
+ resource_name resource_name
36
+ include adapter.const_get('ManagerMethods') if adapter
37
+ class_eval &block if block_given?
38
+ end
39
+ include adapter.const_get('ResourceMethods') if adapter
40
+
41
+ # Callbacks
42
+ state_manager_added(property, klass, options) if respond_to? :state_manager_added
43
+ klass.added_to_resource(self, property, options)
44
+
45
+ # Define the subclass as a constant. We do this for multiple reasons, one
46
+ # of which is to allow it to be serialized to YAML for delayed_job
47
+ const_name = "#{property.to_s.camelize}States"
48
+ remove_const const_name if const_defined?(const_name)
49
+ const_set(const_name, klass)
50
+
51
+ # Create an accessor for the state manager on this resource
52
+ state_managers[property] = klass
53
+ property_name = "#{property.to_s}_manager"
54
+ define_method property_name do
55
+ self.state_managers ||= {}
56
+ state_manager = state_managers[property]
57
+ unless state_manager
58
+ state_manager = klass.new(self)
59
+ state_managers[property] = state_manager
60
+ end
61
+ state_manager
62
+ end
63
+
64
+ # Define the helper methods on the resource
65
+ Helpers::Methods.define_methods(klass.specification, self, property) if options[:helpers]
66
+ end
67
+
68
+ module InstanceMethods
69
+ # Ensures that all properties with state managers are in valid states
70
+ def validate_states!
71
+ self.state_managers ||= {}
72
+ self.class.state_managers.each do |name, klass|
73
+ # Simply ensuring that all of the state managers have been
74
+ # instantiated will make the corresponding states valid
75
+ unless state_managers[name]
76
+ state_managers[name] = klass.new(self)
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ end
83
+ end
@@ -0,0 +1,146 @@
1
+ require 'active_support/core_ext'
2
+
3
+ module StateManager
4
+ class State
5
+
6
+ # Represents the static specification of this state. This consists of all
7
+ # child states and events. During initialization, the specification will
8
+ # be read and the child states and events will be initialized.
9
+ class Specification
10
+ attr_accessor :states, :events, :initial_state
11
+
12
+ def initialize
13
+ self.states = {}
14
+ self.events = {}
15
+ end
16
+
17
+ def initialize_copy(source)
18
+ self.states = source.states.dup
19
+ self.events = source.events.dup
20
+ end
21
+ end
22
+
23
+ class_attribute :specification
24
+ self.specification = Specification.new
25
+
26
+ def self.inherited(child)
27
+ # Give all sublcasses a clone of this states specification. Subclasses can
28
+ # add events and states to their specification without affecting the
29
+ # parent
30
+ child.specification = specification.clone
31
+ end
32
+
33
+ attr_reader :name, :states, :parent_state
34
+
35
+ def initialize(name, parent_state)
36
+ self.name = name
37
+ self.parent_state = parent_state
38
+ self.states = self.class.specification.states.inject({}) do |states, (name, klazz)|
39
+ states[name] = klazz.new(name, self)
40
+ states
41
+ end
42
+ end
43
+
44
+ # String representing the path of the current state, e.g.:
45
+ # 'parentState.childState'
46
+ def path
47
+ path = name.to_s
48
+ path = "#{parent_state.path}.#{path}" if parent_state && parent_state.name
49
+ path
50
+ end
51
+
52
+ def enter
53
+ end
54
+
55
+ def exit
56
+ end
57
+
58
+ def entered
59
+ end
60
+
61
+ def exited
62
+ end
63
+
64
+ def to_s
65
+ "#{path}"
66
+ end
67
+
68
+ def to_sym
69
+ path.to_sym
70
+ end
71
+
72
+ def state_manager
73
+ parent_state.state_manager
74
+ end
75
+
76
+ # Get the resource stored on the state manager
77
+ def resource
78
+ state_manager.resource
79
+ end
80
+
81
+ def transition_to(*args)
82
+ state_manager.transition_to(*args)
83
+ end
84
+
85
+ def has_event?(name)
86
+ name = name.to_sym
87
+ !!self.class.specification.events[name]
88
+ end
89
+
90
+ def send_event(name, *args)
91
+ name = name.to_sym
92
+ event = self.class.specification.events[name]
93
+ send(name, *args) if respond_to?(name)
94
+ transition_to(event[:transitions_to]) if event[:transitions_to]
95
+ end
96
+
97
+ # Find all the states along the path
98
+ def find_states(path)
99
+ state = self
100
+ parts = path.split('.')
101
+ ret = [state]
102
+ parts.each do |name|
103
+ state = state.states[name.to_sym]
104
+ ret << state
105
+ return unless state
106
+ end
107
+ ret
108
+ end
109
+
110
+ # Returns the state at the given path
111
+ def find_state(path)
112
+ states = find_states(path)
113
+ states && states.last
114
+ end
115
+
116
+ def leaf?
117
+ states.empty?
118
+ end
119
+
120
+ # If an initial state is not explicitly specified, we choose the first leaf
121
+ # state
122
+ def initial_state
123
+ if state = self.class.specification.initial_state
124
+ find_state(state.to_s)
125
+ elsif leaf?
126
+ self
127
+ else
128
+ states.values.first.initial_state
129
+ end
130
+ end
131
+
132
+ def self.create_resource_accessor!(name)
133
+ unless method_defined?(name)
134
+ define_method name do
135
+ resource
136
+ end
137
+ end
138
+ specification.states.values.each {|s|s.create_resource_accessor!(name)}
139
+ end
140
+
141
+ protected
142
+
143
+ attr_writer :name, :states, :parent_state
144
+
145
+ end
146
+ end
@@ -0,0 +1 @@
1
+ require 'state_manager/core'
@@ -0,0 +1,102 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "state_manager"
8
+ s.version = "0.2.3"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Gordon Hempton"]
12
+ s.date = "2012-06-06"
13
+ s.description = "Finite state machine implementation that keeps logic separate from model classes and supports sub-states."
14
+ s.email = "ghempton@gmail.com"
15
+ s.extra_rdoc_files = [
16
+ "LICENSE.txt",
17
+ "README.md"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ "Gemfile",
22
+ "Gemfile.lock",
23
+ "LICENSE.txt",
24
+ "README.md",
25
+ "Rakefile",
26
+ "VERSION",
27
+ "lib/state_manager.rb",
28
+ "lib/state_manager/adapters.rb",
29
+ "lib/state_manager/adapters/active_record.rb",
30
+ "lib/state_manager/adapters/base.rb",
31
+ "lib/state_manager/base.rb",
32
+ "lib/state_manager/core.rb",
33
+ "lib/state_manager/dsl.rb",
34
+ "lib/state_manager/helpers.rb",
35
+ "lib/state_manager/plugins.rb",
36
+ "lib/state_manager/plugins/delayed_job.rb",
37
+ "lib/state_manager/resource.rb",
38
+ "lib/state_manager/state.rb",
39
+ "state_manager.gemspec",
40
+ "test/adapters/active_record_test.rb",
41
+ "test/basic_test.rb",
42
+ "test/definition_test.rb",
43
+ "test/helper.rb",
44
+ "test/helpers_test.rb",
45
+ "test/plugins/delayed_job_test.rb",
46
+ "test/transitions_test.rb"
47
+ ]
48
+ s.homepage = "http://github.com/ghempton/statemanager"
49
+ s.licenses = ["MIT"]
50
+ s.require_paths = ["lib"]
51
+ s.rubygems_version = "1.8.15"
52
+ s.summary = "%Q{Finite state machine implementation.}"
53
+
54
+ if s.respond_to? :specification_version then
55
+ s.specification_version = 3
56
+
57
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
58
+ s.add_runtime_dependency(%q<activesupport>, [">= 0"])
59
+ s.add_development_dependency(%q<rdoc>, ["~> 3.12"])
60
+ s.add_development_dependency(%q<pry>, [">= 0"])
61
+ s.add_development_dependency(%q<pry-doc>, [">= 0"])
62
+ s.add_development_dependency(%q<pry-remote>, [">= 0"])
63
+ s.add_development_dependency(%q<pry-nav>, [">= 0"])
64
+ s.add_development_dependency(%q<pry-stack_explorer>, [">= 0"])
65
+ s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
66
+ s.add_development_dependency(%q<jeweler>, ["~> 1.8.3"])
67
+ s.add_development_dependency(%q<delayed_job_active_record>, [">= 0"])
68
+ s.add_development_dependency(%q<activerecord>, [">= 0"])
69
+ s.add_development_dependency(%q<sqlite3>, [">= 0"])
70
+ s.add_development_dependency(%q<timecop>, [">= 0"])
71
+ else
72
+ s.add_dependency(%q<activesupport>, [">= 0"])
73
+ s.add_dependency(%q<rdoc>, ["~> 3.12"])
74
+ s.add_dependency(%q<pry>, [">= 0"])
75
+ s.add_dependency(%q<pry-doc>, [">= 0"])
76
+ s.add_dependency(%q<pry-remote>, [">= 0"])
77
+ s.add_dependency(%q<pry-nav>, [">= 0"])
78
+ s.add_dependency(%q<pry-stack_explorer>, [">= 0"])
79
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
80
+ s.add_dependency(%q<jeweler>, ["~> 1.8.3"])
81
+ s.add_dependency(%q<delayed_job_active_record>, [">= 0"])
82
+ s.add_dependency(%q<activerecord>, [">= 0"])
83
+ s.add_dependency(%q<sqlite3>, [">= 0"])
84
+ s.add_dependency(%q<timecop>, [">= 0"])
85
+ end
86
+ else
87
+ s.add_dependency(%q<activesupport>, [">= 0"])
88
+ s.add_dependency(%q<rdoc>, ["~> 3.12"])
89
+ s.add_dependency(%q<pry>, [">= 0"])
90
+ s.add_dependency(%q<pry-doc>, [">= 0"])
91
+ s.add_dependency(%q<pry-remote>, [">= 0"])
92
+ s.add_dependency(%q<pry-nav>, [">= 0"])
93
+ s.add_dependency(%q<pry-stack_explorer>, [">= 0"])
94
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
95
+ s.add_dependency(%q<jeweler>, ["~> 1.8.3"])
96
+ s.add_dependency(%q<delayed_job_active_record>, [">= 0"])
97
+ s.add_dependency(%q<activerecord>, [">= 0"])
98
+ s.add_dependency(%q<sqlite3>, [">= 0"])
99
+ s.add_dependency(%q<timecop>, [">= 0"])
100
+ end
101
+ end
102
+