flow_machine 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +177 -0
- data/Rakefile +38 -0
- data/lib/flow_machine.rb +12 -0
- data/lib/flow_machine/callback.rb +43 -0
- data/lib/flow_machine/change_callback.rb +11 -0
- data/lib/flow_machine/factory.rb +27 -0
- data/lib/flow_machine/state_callback.rb +5 -0
- data/lib/flow_machine/version.rb +3 -0
- data/lib/flow_machine/workflow.rb +206 -0
- data/lib/flow_machine/workflow_state.rb +162 -0
- data/lib/tasks/workflow_tasks.rake +4 -0
- data/spec/flow_machine/factory_spec.rb +48 -0
- data/spec/flow_machine/multiple_workflow_spec.rb +37 -0
- data/spec/flow_machine/workflow_callback_spec.rb +131 -0
- data/spec/flow_machine/workflow_change_callback_spec.rb +26 -0
- data/spec/flow_machine/workflow_spec.rb +202 -0
- data/spec/flow_machine/workflow_state_spec.rb +253 -0
- data/spec/spec_helper.rb +94 -0
- metadata +112 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 0e9a4fd3c30a8e7920333c34c4cb23b075648f91
|
4
|
+
data.tar.gz: 6df24793c54c72d1498ab49b40da25b3aa29820f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4d287bcf3788aeec09232337a905d8c851065c0e2067455480f95308dd764d8c8567e5b11beadf316919e64ee94c3921ec9d2035afb6c014bc43205820433ae6
|
7
|
+
data.tar.gz: cfc7ec61704bb070cf216704edd496f5d1719f3f4c52c026c6e92dd1de013ea4ab9ce4bdcc72e4bd18eb85a019e1ad2c5ff3536ed76d8a4d65d3c5be2e6b5a7d
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2015 Table XI. http://www.tablexi.com
|
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,177 @@
|
|
1
|
+
# FlowMachine
|
2
|
+
|
3
|
+
Build finite state machines in a backend-agnostic, class-centric way.
|
4
|
+
|
5
|
+
The basic features will work with any PORO, and more features and callbacks are available when used with an ORM like `ActiveRecord` and/or `ActiveModel::Dirty`.
|
6
|
+
|
7
|
+
## *Raison d'être*
|
8
|
+
|
9
|
+
After exploring several of the existing Ruby state machine options, they all seem too tightly coupled to an ORM models and tend to pollute the object model's code far too much. The goal of this gem is to provide a clean, testable interface for working with a state machine that decouples as much as possible from the model object itself.
|
10
|
+
|
11
|
+
## Simple Usage
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
class BlogPost
|
15
|
+
attr_accessor :state, :title, :body, :author
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
self.state = :draft
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class PublishingWorkflow
|
23
|
+
include FlowMachine::Workflow
|
24
|
+
|
25
|
+
state DraftState
|
26
|
+
state PublishedState
|
27
|
+
end
|
28
|
+
|
29
|
+
class DraftState < FlowMachine::WorkflowState
|
30
|
+
event :publish do
|
31
|
+
transition to: :published
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class PublishedState < FlowMachine::WorkflowState
|
36
|
+
on_enter :notify_email_author
|
37
|
+
|
38
|
+
def notify_email_author
|
39
|
+
# Send an email
|
40
|
+
end
|
41
|
+
end
|
42
|
+
```
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
blog_post = BlogPost.new
|
46
|
+
blog_post.author = "author@example.org"
|
47
|
+
workflow = PublishingWorkflow.new(blog_post)
|
48
|
+
workflow.publish # notify_email_author is called (returns true if successful)
|
49
|
+
workflow.published? # => true
|
50
|
+
blog_post.state # => :published
|
51
|
+
```
|
52
|
+
|
53
|
+
|
54
|
+
## `transition!`
|
55
|
+
|
56
|
+
If you are using the workflow around an ORM model like ActiveRecord, calling the bang version of the transition will perform the transition and call `save` on the object. This method will return the value returned by `save` (`true`/`false` for ActiveRecord) or `false` if any of the guards fail.
|
57
|
+
|
58
|
+
E.g. `workflow.publish!` will transition the object to `published` and call `save` on the object.
|
59
|
+
|
60
|
+
## Guards
|
61
|
+
|
62
|
+
Guards are used to allow or prevent `event`s from being called. `:guard` accepts a single symbol or an array of symbols representing methods. The method may be on the state, the workflow, or the object itself, and the method will be searched for in that order.
|
63
|
+
|
64
|
+
**Best practice** Use predicate methods that return a simple true/false. All guard methods are called, so avoid side affects in these methods.
|
65
|
+
|
66
|
+
Calling the transition with a failing guard will result in the object not being transitioned and returning `false`. If using the bang version, `save` will not be called.
|
67
|
+
|
68
|
+
#### may_xxx?
|
69
|
+
|
70
|
+
`workflow.may_publish?` will call all the guard methods and return `false` if any of the guard methods return `false`. It will also return `false` if you are not in a state that has a defined event (e.g. `published_workflow.may_publish?` will always return `false`)
|
71
|
+
|
72
|
+
#### guard_errors
|
73
|
+
|
74
|
+
After calling `may_xxx?`, the workflow will have an array of the guard methods that failed. to avoid additional dependencies, the developer is responsible for converting these to human readable messages (using I18n or the like).
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
class DraftState < FlowMachine::WorkflowState
|
78
|
+
event :publish, guard: [:content_present?, :can_publish?]
|
79
|
+
transition to: :published
|
80
|
+
end
|
81
|
+
|
82
|
+
def can_publish?
|
83
|
+
false
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
class BlogPost
|
88
|
+
def content_present?
|
89
|
+
content.present? # assuming you have ActiveSupport loaded
|
90
|
+
end
|
91
|
+
end
|
92
|
+
```
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
workflow.may_publish? # => false
|
96
|
+
workfow.guard_errors # => [:can_publish?]
|
97
|
+
workflow.publish # => false
|
98
|
+
```
|
99
|
+
|
100
|
+
## Callbacks
|
101
|
+
|
102
|
+
State and Workflow callbacks accept `if` and `unless` options. They may be a symbol or array of symbols (looking for the method in the state, workflow, and object in that order) or a Proc.
|
103
|
+
|
104
|
+
### State callbacks
|
105
|
+
|
106
|
+
Declared in the `WorkflowState` class.
|
107
|
+
|
108
|
+
* `on_enter` Called after the object has transitioned into the state
|
109
|
+
|
110
|
+
The following are available when `Workflow#save` is used (`workflow.save` or `workflow.transition!`) *Not called if you call `save` directly on the decorated model*.
|
111
|
+
|
112
|
+
* `after_enter` Called when the object has transitioned into the state and the object has been saved either `workflow.save` or `workflow.transition!` has been called.
|
113
|
+
* `before_change` Useful when watching for changes to a model, but only when in a certain state. Will be called if anything exists in the `object#changes` hash (if it exists), often provided by `ActiveModel#dirty`.
|
114
|
+
* `after_change` Useful when watching for changes to a model in a certain state, but you only want to trigger when the save is successful (e.g. the model is `valid?`)
|
115
|
+
|
116
|
+
### Workflow callbacks
|
117
|
+
|
118
|
+
Declared in the `Workflow` class.
|
119
|
+
|
120
|
+
* `after_transition` Called anytime a transition takes place
|
121
|
+
|
122
|
+
The following are available when `Workflow#save` is used:
|
123
|
+
|
124
|
+
* `before_save` Called when `Workflow#save` is called, but before `object#save` is called
|
125
|
+
* `after_save` Called after `object#save` has returned `true`
|
126
|
+
|
127
|
+
### Transition callbacks
|
128
|
+
|
129
|
+
Declared as an option to the `transition` method inside an `event` block.
|
130
|
+
|
131
|
+
* `after` Will be called after the transition has happened successfully. Useful when you only want something to trigger when moving from a specific state to another.
|
132
|
+
|
133
|
+
`transition to: :published, after: :send_mailing_list_email`
|
134
|
+
|
135
|
+
## Scopes and Predicate methods
|
136
|
+
|
137
|
+
If you want scopes and predicate methods defined on your model, use the following:
|
138
|
+
|
139
|
+
`PublishingWorkflow.create_scopes_on(self)` within the model.
|
140
|
+
|
141
|
+
Assuming BlogPost is an ActiveRecord model, this will create `BlogPost.draft` and `BlogPost.published` scopes as well as the `BlogPost#draft?` and `BlogPost#published?` methods.
|
142
|
+
|
143
|
+
## Other useful features
|
144
|
+
|
145
|
+
### Use a different attribute for the state
|
146
|
+
|
147
|
+
If you don't want to use `state` as your field for storing state, simply declare `state_attribute :status` in the Workflow class.
|
148
|
+
|
149
|
+
### List of state names
|
150
|
+
|
151
|
+
```ruby
|
152
|
+
PublishingWorkflow.state_names` # => ['draft', 'published']
|
153
|
+
```
|
154
|
+
|
155
|
+
Especially useful in an ActiveModel validation:
|
156
|
+
|
157
|
+
```ruby
|
158
|
+
validates :state, presence: true, inclusion: { in: PublishingWorkflow.state_names }
|
159
|
+
```
|
160
|
+
|
161
|
+
### Options Hash
|
162
|
+
|
163
|
+
You can pass an options hash into the workflow which is available at any time while using the workflow. A prime example is tracking the user who performed an action.
|
164
|
+
|
165
|
+
```ruby
|
166
|
+
class PublishedState < FlowMachine::WorkflowState
|
167
|
+
on_enter :update_published_by
|
168
|
+
|
169
|
+
def update_published_by
|
170
|
+
object.published_by = options[:current_user]
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
workflow = PublishingWorkflow.new(blog_post, current_user: User.find(123))
|
175
|
+
workflow.published
|
176
|
+
blog_post.published_by # => User #123
|
177
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'Workflow'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.rdoc')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
|
19
|
+
|
20
|
+
Bundler::GemHelper.install_tasks
|
21
|
+
|
22
|
+
require 'rake/testtask'
|
23
|
+
|
24
|
+
Rake::TestTask.new(:test) do |t|
|
25
|
+
t.libs << 'lib'
|
26
|
+
t.libs << 'test'
|
27
|
+
t.pattern = 'test/**/*_test.rb'
|
28
|
+
t.verbose = false
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
task default: :test
|
33
|
+
|
34
|
+
begin
|
35
|
+
require 'rspec/core/rake_task'
|
36
|
+
RSpec::Core::RakeTask.new(:spec)
|
37
|
+
rescue LoadError
|
38
|
+
end
|
data/lib/flow_machine.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__))
|
2
|
+
|
3
|
+
require "flow_machine/workflow"
|
4
|
+
require "flow_machine/factory"
|
5
|
+
require "flow_machine/workflow_state"
|
6
|
+
require "flow_machine/callback"
|
7
|
+
require "flow_machine/state_callback"
|
8
|
+
require "flow_machine/change_callback"
|
9
|
+
|
10
|
+
module FlowMachine
|
11
|
+
end
|
12
|
+
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'active_support/core_ext/object/blank'
|
2
|
+
require 'active_support/core_ext/array/extract_options'
|
3
|
+
|
4
|
+
class FlowMachine::Callback
|
5
|
+
attr_accessor :method, :options
|
6
|
+
def initialize(*args, &block)
|
7
|
+
@options = args.extract_options!
|
8
|
+
@method = args.shift unless block
|
9
|
+
@block = block
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(target, changes = {})
|
13
|
+
return unless will_run? target, changes
|
14
|
+
call!(target)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Runs the callback without any validations
|
18
|
+
def call!(target)
|
19
|
+
run_method_or_lambda(target, method.presence || @block)
|
20
|
+
end
|
21
|
+
|
22
|
+
def run_method_or_lambda(target, method)
|
23
|
+
if method.respond_to? :call # is it a lambda
|
24
|
+
target.instance_exec &method
|
25
|
+
else
|
26
|
+
run_method(target, method)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def run_method(target, method)
|
31
|
+
target.send(method)
|
32
|
+
end
|
33
|
+
|
34
|
+
def will_run?(target, changes = {})
|
35
|
+
if options[:if]
|
36
|
+
[*options[:if]].all? { |m| run_method_or_lambda(target, m) }
|
37
|
+
elsif options[:unless]
|
38
|
+
[*options[:unless]].none? { |m| run_method_or_lambda(target, m) }
|
39
|
+
else
|
40
|
+
true
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class FlowMachine::ChangeCallback < FlowMachine::StateCallback
|
2
|
+
attr_accessor :field
|
3
|
+
def initialize(field, *args, &block)
|
4
|
+
@field = field
|
5
|
+
super(*args, &block)
|
6
|
+
end
|
7
|
+
|
8
|
+
def will_run?(object, changes = {})
|
9
|
+
changes.keys.include?(field.to_s) && super
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module FlowMachine
|
2
|
+
class Factory
|
3
|
+
def self.workflow_for(object, options = {})
|
4
|
+
# If the object is enumerable, delegate. This allows workflow_for
|
5
|
+
# as shorthand
|
6
|
+
return workflow_for_collection(object, options) if object.respond_to?(:map)
|
7
|
+
|
8
|
+
klazz = workflow_class_for(object)
|
9
|
+
return nil unless klazz
|
10
|
+
klazz.new(object, options)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.workflow_for_collection(collection, options = {})
|
14
|
+
collection.map { |item| workflow_for(item, options) }
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.workflow_class_for(object_or_class)
|
18
|
+
if object_or_class.is_a? Class
|
19
|
+
"#{object_or_class.name}Workflow".constantize
|
20
|
+
else
|
21
|
+
workflow_class_for(object_or_class.class)
|
22
|
+
end
|
23
|
+
rescue NameError # if the workflow class doesn't exist
|
24
|
+
nil
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,206 @@
|
|
1
|
+
require "active_support/core_ext/module/delegation"
|
2
|
+
require "active_support/core_ext/object/try"
|
3
|
+
|
4
|
+
module FlowMachine
|
5
|
+
module Workflow
|
6
|
+
def self.included(base)
|
7
|
+
base.extend(ClassMethods)
|
8
|
+
base.send(:attr_reader, :object)
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
# resolves to
|
13
|
+
# class Model
|
14
|
+
# def self.published
|
15
|
+
# where(status: 'published')
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# def published?
|
19
|
+
# self.status == 'published'
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
def create_scopes_on(content_class)
|
23
|
+
self.state_names.each do |status|
|
24
|
+
content_class.singleton_class.send(:define_method, status) do
|
25
|
+
where(status: status)
|
26
|
+
end
|
27
|
+
|
28
|
+
content_class.send(:define_method, "#{status}?") do
|
29
|
+
self.status == status
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
attr_accessor :callbacks
|
35
|
+
|
36
|
+
def state_names
|
37
|
+
states.keys.map &:to_s
|
38
|
+
end
|
39
|
+
|
40
|
+
def states
|
41
|
+
@states ||= {}
|
42
|
+
end
|
43
|
+
|
44
|
+
# Mainly to be used in testing, call this method to ensure that
|
45
|
+
# any dynamically created state methods get exposed to the workflow
|
46
|
+
def refresh_state_methods!
|
47
|
+
states.values.each do |state_class|
|
48
|
+
add_state_methods_from(state_class)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def state(state_class)
|
53
|
+
name = get_state_name(state_class)
|
54
|
+
states[name] = state_class
|
55
|
+
add_state_methods_from(state_class)
|
56
|
+
|
57
|
+
define_method "#{name}?" do
|
58
|
+
current_state_name.to_s == name.to_s
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def state_attribute(method)
|
63
|
+
@state_attribute = method
|
64
|
+
end
|
65
|
+
|
66
|
+
def state_method
|
67
|
+
@state_attribute || :state
|
68
|
+
end
|
69
|
+
|
70
|
+
def before_save(*args, &block)
|
71
|
+
add_callback(:before_save, FlowMachine::Callback.new(*args, &block))
|
72
|
+
end
|
73
|
+
|
74
|
+
def after_save(*args, &block)
|
75
|
+
add_callback(:after_save, FlowMachine::Callback.new(*args, &block))
|
76
|
+
end
|
77
|
+
|
78
|
+
def after_transition(*args, &block)
|
79
|
+
add_callback(:after_transition, FlowMachine::Callback.new(*args, &block))
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def add_callback(hook, callback)
|
85
|
+
self.callbacks ||= {}
|
86
|
+
callbacks[hook] ||= []
|
87
|
+
callbacks[hook] << callback
|
88
|
+
end
|
89
|
+
|
90
|
+
# Defines an instance method on Workflow that delegates to the
|
91
|
+
# current state, but only if the current_state implements the method
|
92
|
+
def define_state_method(method_name)
|
93
|
+
define_method method_name do |*args|
|
94
|
+
current_state.respond_to?(method_name) && current_state.send(method_name, *args)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def add_state_methods_from(state_class)
|
99
|
+
state_class.expose_to_workflow_methods.try(:each) do |method_name|
|
100
|
+
define_state_method(method_name) unless method_defined?(method_name)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def get_state_name(state_class)
|
105
|
+
state_class.state_name
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
attr_accessor :options, :previous_state_persistence_callbacks, :previous_state
|
110
|
+
attr_accessor :changes
|
111
|
+
|
112
|
+
# extend Forwardable
|
113
|
+
# def_delegators :current_state, :guard_errors
|
114
|
+
delegate :guard_errors, to: :current_state
|
115
|
+
|
116
|
+
def initialize(object, options = {})
|
117
|
+
@object = object
|
118
|
+
@options = options
|
119
|
+
@previous_state_persistence_callbacks = []
|
120
|
+
end
|
121
|
+
|
122
|
+
def current_state_name
|
123
|
+
object.send(self.class.state_method)
|
124
|
+
end
|
125
|
+
alias_method :state, :current_state_name
|
126
|
+
|
127
|
+
def previous_state_name
|
128
|
+
@previous_state.try(:name)
|
129
|
+
end
|
130
|
+
|
131
|
+
def current_state
|
132
|
+
@current_state ||= self.class.states[current_state_name.to_sym].new(self)
|
133
|
+
end
|
134
|
+
|
135
|
+
def transition(options = {})
|
136
|
+
@previous_state = current_state
|
137
|
+
@current_state = nil
|
138
|
+
self.current_state_name = options[:to].to_s if options[:to]
|
139
|
+
@previous_state_persistence_callbacks << FlowMachine::StateCallback.new(options[:after]) if options[:after]
|
140
|
+
fire_callbacks(:after_transition) unless previous_state == current_state
|
141
|
+
current_state
|
142
|
+
end
|
143
|
+
|
144
|
+
def save
|
145
|
+
persist
|
146
|
+
end
|
147
|
+
|
148
|
+
def persist
|
149
|
+
self.changes = object.changes
|
150
|
+
# If the model has a default state from the database, then it doesn't get
|
151
|
+
# included in `changes` when you're first saving it.
|
152
|
+
self.changes[state_method.to_s] ||= [nil, current_state_name] if object.new_record?
|
153
|
+
|
154
|
+
fire_callbacks(:before_save)
|
155
|
+
current_state.fire_callbacks(:before_change, changes)
|
156
|
+
|
157
|
+
if persist_object
|
158
|
+
fire_state_callbacks
|
159
|
+
fire_callbacks(:after_save)
|
160
|
+
true
|
161
|
+
else
|
162
|
+
self.current_state_name = @previous_state.name.to_s if @previous_state
|
163
|
+
false
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# Useful for using in if/unless on state and after_save callbacks so you can
|
168
|
+
# run the callback only on the initial persistence
|
169
|
+
def create?
|
170
|
+
self.changes[state_method.to_s].try(:first).blank?
|
171
|
+
end
|
172
|
+
|
173
|
+
def persist_object
|
174
|
+
object.save
|
175
|
+
end
|
176
|
+
|
177
|
+
def current_state_name=(new_state)
|
178
|
+
raise ArgumentError.new("invalid state: #{new_state}") unless self.class.state_names.include?(new_state.to_s)
|
179
|
+
object.send("#{self.class.state_method}=", new_state)
|
180
|
+
end
|
181
|
+
|
182
|
+
def current_user
|
183
|
+
options[:current_user]
|
184
|
+
end
|
185
|
+
|
186
|
+
private
|
187
|
+
|
188
|
+
def fire_callbacks(event)
|
189
|
+
self.class.callbacks ||= {}
|
190
|
+
self.class.callbacks[event].try(:each) do |callback|
|
191
|
+
callback.call(self, changes)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def fire_state_callbacks
|
196
|
+
previous_state.fire_callback_list(previous_state_persistence_callbacks) if previous_state
|
197
|
+
@previous_state_persistence_callbacks = []
|
198
|
+
current_state.fire_callbacks(:after_enter, changes) if changes.include? state_method.to_s
|
199
|
+
current_state.fire_callbacks(:after_change, changes)
|
200
|
+
end
|
201
|
+
|
202
|
+
def state_method
|
203
|
+
self.class.state_method
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|