state_manager 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/Gemfile ADDED
@@ -0,0 +1,18 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem 'activesupport'
4
+
5
+ group :development do
6
+ gem "rdoc", "~> 3.12"
7
+ gem 'pry'
8
+ gem 'pry-doc'
9
+ gem 'pry-remote'
10
+ gem 'pry-nav'
11
+ gem 'pry-stack_explorer'
12
+ gem "bundler", "~> 1.0.0"
13
+ gem "jeweler", "~> 1.8.3"
14
+ gem 'delayed_job_active_record'
15
+ gem 'activerecord'
16
+ gem 'sqlite3'
17
+ gem 'timecop'
18
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,74 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ activemodel (3.2.3)
5
+ activesupport (= 3.2.3)
6
+ builder (~> 3.0.0)
7
+ activerecord (3.2.3)
8
+ activemodel (= 3.2.3)
9
+ activesupport (= 3.2.3)
10
+ arel (~> 3.0.2)
11
+ tzinfo (~> 0.3.29)
12
+ activesupport (3.2.3)
13
+ i18n (~> 0.6)
14
+ multi_json (~> 1.0)
15
+ arel (3.0.2)
16
+ binding_of_caller (0.6.7)
17
+ builder (3.0.0)
18
+ coderay (1.0.6)
19
+ delayed_job (3.0.1)
20
+ activesupport (~> 3.0)
21
+ delayed_job_active_record (0.3.2)
22
+ activerecord (> 2.1.0)
23
+ delayed_job (~> 3.0.0)
24
+ git (1.2.5)
25
+ i18n (0.6.0)
26
+ jeweler (1.8.3)
27
+ bundler (~> 1.0)
28
+ git (>= 1.2.5)
29
+ rake
30
+ rdoc
31
+ json (1.7.3)
32
+ method_source (0.7.1)
33
+ multi_json (1.3.5)
34
+ pry (0.9.9.6)
35
+ coderay (~> 1.0.5)
36
+ method_source (~> 0.7.1)
37
+ slop (>= 2.4.4, < 3)
38
+ pry-doc (0.4.2)
39
+ pry (>= 0.9.0)
40
+ yard (~> 0.8.1)
41
+ pry-nav (0.2.1)
42
+ pry (~> 0.9.9)
43
+ pry-remote (0.1.4)
44
+ pry (~> 0.9.9)
45
+ slop (~> 2.1)
46
+ pry-stack_explorer (0.4.2)
47
+ binding_of_caller (~> 0.6.2)
48
+ pry (~> 0.9.9)
49
+ rake (0.9.2.2)
50
+ rdoc (3.12)
51
+ json (~> 1.4)
52
+ slop (2.4.4)
53
+ sqlite3 (1.3.6)
54
+ timecop (0.3.5)
55
+ tzinfo (0.3.33)
56
+ yard (0.8.1)
57
+
58
+ PLATFORMS
59
+ ruby
60
+
61
+ DEPENDENCIES
62
+ activerecord
63
+ activesupport
64
+ bundler (~> 1.0.0)
65
+ delayed_job_active_record
66
+ jeweler (~> 1.8.3)
67
+ pry
68
+ pry-doc
69
+ pry-nav
70
+ pry-remote
71
+ pry-stack_explorer
72
+ rdoc (~> 3.12)
73
+ sqlite3
74
+ timecop
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 Gordon Hempton
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,241 @@
1
+ # State Manager
2
+
3
+ StateManager is a state machine implementation for Ruby that is heavily inspired by the FSM implementation in [Ember.js](http://emberjs.com). Compared to other FSM implementations, it has the following defining characteristics:
4
+
5
+ * Sub-states are supported (e.g. 'submitted.reviewing').
6
+ * State logic can be kept separate from model classes.
7
+ * State definitions are modular, underlying each state is a separate class definition.
8
+ * Supports integrations. Comes out of the box with an integration with active_record and delayed_job to support automatic delayed transtions.
9
+
10
+ We believe this is an improvement over existing state machines, but just for good measure, check out [state_machine](https://github.com/pluginaweek/state_machine) and [workflow](https://github.com/geekq/workflow).
11
+
12
+ ## Getting Started
13
+
14
+ Install the `state_manager` gem. If you are using bundler, add the following to your Gemfile:
15
+
16
+ ```
17
+ gem 'state_manager'
18
+ ```
19
+
20
+ After the gem is installed, create a file to contain the definition of your state manager, e.g. `app/states/post_states.rb`. Edit this file and define your states:
21
+
22
+ ```ruby
23
+ class PostStates < StateManager::Base
24
+ state :unsubmitted do
25
+ event :submit, :transitions_to => 'submitted.awaiting_review'
26
+ end
27
+ state :submitted
28
+ event :reject, :transitions_to => 'rejected'
29
+ state :awaiting_review do
30
+ event :review, :transitions_to => 'submitted.reviewing'
31
+ end
32
+ state :reviewing do
33
+ event :accept, :transitions_to => 'active'
34
+ event :clarify, :transitions_to => 'submitted.clarifying'
35
+ end
36
+ state :clarifying do
37
+ event :review, :transitions_to => 'submitted.reviewing'
38
+ end
39
+ end
40
+ state :active
41
+ state :rejected
42
+ end
43
+ ```
44
+
45
+ Once your states are defined, you need to extend the `StateManager::Resource` module on your resource class and define a state managed property:
46
+
47
+ ```ruby
48
+ class Post
49
+ attr_accessor :state
50
+ extend StateManager::Resource
51
+ state_manager
52
+ end
53
+ ```
54
+
55
+ The code above infers the existence of `PostStates` class and a `state` property. An alternate states definition and property could be specified as follows:
56
+
57
+ ```ruby
58
+ class Post
59
+ attr_accessor :workflow_state
60
+ extend StateManager::Resource
61
+ state_manager :workflow_state, PostStates
62
+ end
63
+ ```
64
+
65
+ ## Helper Methods
66
+
67
+ Unless otherwise specified with `{:helpers => false}` as the third argument to the `state_manager` macro, helper methods will be added to the resource class. In the above example, some of the methods that will be available are:
68
+
69
+ ```ruby
70
+ post = Post.new
71
+ post.unsubmitted? # true, the post will initially be in the 'unsubmitted' state
72
+ post.can_submit? # true, the 'submit' event is defined on the current state
73
+ post.can_review? # false, the 'review' event is not defined on the current state
74
+ post.submit! # invokes the submit event
75
+ post.submitted_awaiting_review? # true, the post is in the 'submitted.awaiting_review' state
76
+ post.submitted? # true, the 'submitted' state is a parent of the current state
77
+ ```
78
+
79
+ ## Handling Events
80
+
81
+ Most applications will require special logic to be performed during state transitions. Handlers for events can be defined as follows:
82
+
83
+ ```ruby
84
+ class PostStates < StateManager::Base
85
+ state :unsubmitted do
86
+ event :submit, :transitions_to => 'submitted.awaiting_review'
87
+ end
88
+ state :submitted
89
+ event :reject, :transitions_to => 'rejected'
90
+ state :awaiting_review do
91
+ event :review, :transitions_to => 'submitted.reviewing'
92
+ end
93
+ state :reviewing do
94
+ event :accept, :transitions_to => 'active'
95
+ event :clarify, :transitions_to => 'submitted.clarifying'
96
+ end
97
+ state :clarifying do
98
+ event :review, :transitions_to => 'submitted.reviewing'
99
+ end
100
+ end
101
+ state :active
102
+ state :rejected
103
+
104
+ # Under the hood, the `state` macro creates a class with the same name as the state. Here we add to the definition of that class.
105
+ class Unsubmitted
106
+ # Defines a handler for the submit event. Events can have arguments
107
+ def submit(reason=nil)
108
+ # Do something, the post is available as either the `resource` or `post` property
109
+ end
110
+ end
111
+
112
+ class Submitted
113
+ class AwaitingReview
114
+ # Handles the review event. This will *not* handle the review event for the 'submitted.clarifying' state
115
+ def review
116
+ end
117
+ end
118
+
119
+ # Events on parent states are available to their children.
120
+ def reject
121
+ end
122
+ end
123
+ end
124
+ ```
125
+
126
+ ## Under the Hood
127
+
128
+ As suggested in the above example, states and events really just correspond to classes and methods of the state manager. In fact, the `state` macro is really just syntactic sugar around defining a `StateManager::State` subclass to the current state--the root state manager is also a state.
129
+
130
+ On the resource, the `state_manager` macro makes an instance of the specified `StateManager::Base` subclass available through the "#{property}_manager" attribute on the resource. The above examples of helper methods is essentially syntactic sugar on the following:
131
+
132
+ ```ruby
133
+ post = Post.new
134
+ post.state_manager.in_state?('unsubmitted') # true, the post will initially be in the 'unsubmitted' state
135
+ post.state_manager.respond_to_event?('submit') # true, the 'submit' event is defined on the current state
136
+ post.state_manager.respond_to_event?('review')? # false, the 'review' event is not defined on the current state
137
+ post.state_manager.send_event!('submit') # invokes the submit event
138
+ post.state_manager.in_state?('submitted.awaiting_review')? # true, the post is in the 'submitted.awaiting_review' state
139
+ post.state_manager.in_state?('submitted')? # true, the 'submitted' state is a parent of the current state
140
+ ```
141
+
142
+ Furthermore, only leaf states are valid states for a resource. The state manager can also be explicitly transitioned to a state, however this should normally only be used inside an event handler:
143
+
144
+ ```ruby
145
+ post.state_manager.transition_to('submitted.awaiting_review') # puts the post is in the 'submitted.awaiting_review' state
146
+ post.state_manager.transition_to('submitted') # throws a StateManager::InvalidState error, 'submitted' is not a leaf state
147
+ ```
148
+
149
+ By default, the initial state will be the first state that was defined. This can be customized by setting the initial state:
150
+
151
+ ```ruby
152
+ class PostStates < StateManager::Base
153
+ initial_state :rejected
154
+ ...
155
+ end
156
+ ```
157
+
158
+ The current state can also be accessed from the state manager:
159
+
160
+ ```ruby
161
+ post.state_manager.current_state.name # 'awaiting_review'
162
+ post.state_manager.current_state.path # 'submitted.awaiting_review'
163
+ ```
164
+
165
+ ## Callbacks
166
+
167
+ StateManager has several callbacks that can be hooked into:
168
+
169
+ ```ruby
170
+ class PostStates < StateManager::Base
171
+ state :unsubmitted do
172
+ event :submit, :transitions_to => 'submitted.awaiting_review'
173
+ end
174
+ state :submitted
175
+
176
+ # Called when the 'submitted' state is being entered
177
+ def enter
178
+ end
179
+
180
+ # Called when the 'submitted' state is being exited.
181
+ def exit
182
+ end
183
+
184
+ # After it has been entered
185
+ def entered
186
+ end
187
+
188
+ # After it has been exited
189
+ def exited
190
+ end
191
+
192
+ event :reject, :transitions_to => 'rejected'
193
+ state :awaiting_review do
194
+ event :review, :transitions_to => 'submitted.reviewing'
195
+ end
196
+
197
+ ...
198
+
199
+ # Called before every transition
200
+ def will_transition(from, to, event)
201
+ end
202
+
203
+ # Called after ever transition
204
+ def did_transition(from, to, event)
205
+ end
206
+ end
207
+ ```
208
+
209
+ In the above example, transitioning between 'submitted.awaiting_review' and 'submitted.reviewing' will *not* trigger the the enter/exit callbacks for the 'submitted' state, however it will be called for the two sub-states.
210
+
211
+ ## Delayed Job Integration
212
+
213
+ StateManager comes out of the box with support for [delayed_job](https://github.com/tobi/delayed_job). If delayed_job is available, events can be defined with a `:delay` property which indicates a delay after which the event should automatically be triggered:
214
+
215
+ ```ruby
216
+ class UserStates < StateManager::Base
217
+ state :submitted do
218
+ event :activate, :transitions_to => :active, :delay => 2.days
219
+ end
220
+
221
+ state :active
222
+ end
223
+ ```
224
+
225
+ In this example, the `activate` event will be called by delayed_job automatically after 2 days unless it is called programatically before then.
226
+
227
+ ## Contributing to statemanager
228
+
229
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
230
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
231
+ * Fork the project.
232
+ * Start a feature/bugfix branch.
233
+ * Commit and push until you are happy with your contribution.
234
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
235
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
236
+
237
+ ## Copyright
238
+
239
+ Copyright (c) 2012 Gordon Hempton. See LICENSE.txt for
240
+ further details.
241
+
data/Rakefile ADDED
@@ -0,0 +1,45 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
17
+ gem.name = "state_manager"
18
+ gem.homepage = "http://github.com/ghempton/statemanager"
19
+ gem.license = "MIT"
20
+ gem.summary = "%Q{Finite state machine implementation.}"
21
+ gem.description = %Q{Finite state machine implementation that keeps logic separate from model classes and supports sub-states.}
22
+ gem.email = "ghempton@gmail.com"
23
+ gem.authors = ["Gordon Hempton"]
24
+ # dependencies defined in Gemfile
25
+ end
26
+ Jeweler::RubygemsDotOrgTasks.new
27
+
28
+ require 'rake/testtask'
29
+ Rake::TestTask.new(:test) do |test|
30
+ test.libs << 'lib' << 'test'
31
+ test.pattern = 'test/**/*_test.rb'
32
+ test.verbose = true
33
+ end
34
+
35
+ task :default => :test
36
+
37
+ require 'rdoc/task'
38
+ Rake::RDocTask.new do |rdoc|
39
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
40
+
41
+ rdoc.rdoc_dir = 'rdoc'
42
+ rdoc.title = "statemanager #{version}"
43
+ rdoc.rdoc_files.include('README*')
44
+ rdoc.rdoc_files.include('lib/**/*.rb')
45
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.3
@@ -0,0 +1,52 @@
1
+ module StateManager
2
+ module Adapters
3
+ module ActiveRecord
4
+ include Base
5
+
6
+ def self.matching_ancestors
7
+ %w(ActiveRecord::Base)
8
+ end
9
+
10
+ module ResourceMethods
11
+
12
+ def self.included(base)
13
+ # Make sure that the model is in a valid state before it is saved
14
+ base.before_validation :_validate_states
15
+
16
+ base.extend(ClassMethods)
17
+ end
18
+
19
+ def _validate_states
20
+ self.validate_states!
21
+ end
22
+
23
+ module ClassMethods
24
+ def state_manager_added(property, klass, options)
25
+ class_eval do
26
+ klass.specification.states.keys.each do |state|
27
+ # The connection might not be ready when defining this code is
28
+ # reached so we wrap in a lamda.
29
+ scope state, lambda {
30
+ conn = ::ActiveRecord::Base.connection
31
+ column = conn.quote_column_name klass._state_property
32
+ query = "#{column} = ? OR #{column} LIKE ?"
33
+ like_term = "#{state.to_s}.%"
34
+ where(query, state, like_term)
35
+ }
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ end
42
+
43
+ module ManagerMethods
44
+
45
+ def write_state(value)
46
+ resource.send :update_attribute, self.class._state_property, value.path
47
+ end
48
+
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,49 @@
1
+ module StateManager
2
+ module Adapters
3
+ module Base
4
+
5
+ module ClassMethods
6
+ # The name of the adapter
7
+ def adapter_name
8
+ @adapter_name ||= begin
9
+ name = self.name.split('::').last
10
+ name.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
11
+ name.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
12
+ name.downcase!
13
+ name.to_sym
14
+ end
15
+ end
16
+
17
+ # Whether this adapter is available for the current library. This
18
+ # is only true if the ORM that the adapter is for is currently
19
+ # defined.
20
+ def available?
21
+ matching_ancestors.any? && Object.const_defined?(matching_ancestors[0].split('::')[0])
22
+ end
23
+
24
+ # The list of ancestor names that cause this adapter to matched.
25
+ def matching_ancestors
26
+ []
27
+ end
28
+
29
+ # Whether the adapter should be used for the given class.
30
+ def matches?(klass)
31
+ matches_ancestors?(klass.ancestors.map {|ancestor| ancestor.name})
32
+ end
33
+
34
+ # Whether the adapter should be used for the given list of ancestors.
35
+ def matches_ancestors?(ancestors)
36
+ (ancestors & matching_ancestors).any?
37
+ end
38
+ end
39
+
40
+ def self.included(base)
41
+ return if base < StateManager::Base
42
+ base.class_eval { extend ClassMethods }
43
+ end
44
+
45
+ extend ClassMethods
46
+
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,30 @@
1
+ # Load each available adapter
2
+ require 'state_manager/adapters/base'
3
+ Dir["#{File.dirname(__FILE__)}/adapters/*.rb"].sort.each do |path|
4
+ require "state_manager/adapters/#{File.basename(path)}"
5
+ end
6
+
7
+ module StateManager
8
+
9
+ class AdapterNotFound < StandardError; end;
10
+
11
+ module Adapters
12
+ def self.match(klass)
13
+ all.detect {|adapter| adapter.matches?(klass)}
14
+ end
15
+
16
+ def self.match_ancestors(ancestors)
17
+ all.detect {|adapter| adapter.matches_ancestors?(ancestors)}
18
+ end
19
+
20
+ def self.find_by_name(name)
21
+ all.detect {|adapter| adapter.integration_name == name} || raise(AdapterNotFound.new(name))
22
+ end
23
+
24
+ def self.all
25
+ constants = self.constants.map {|c| c.to_s}.sort
26
+ constants.map {|c| const_get(c)}
27
+ end
28
+ end
29
+
30
+ end