state_manager 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+