statesman 0.1.0 → 0.2.0
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.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -0
- data/.travis.yml +1 -0
- data/CHANGELOG.md +20 -0
- data/README.md +38 -16
- data/lib/statesman/callback.rb +8 -6
- data/lib/statesman/machine.rb +35 -33
- data/lib/statesman/version.rb +1 -1
- data/spec/spec_helper.rb +17 -1
- data/spec/statesman/adapters/mongoid_spec.rb +1 -1
- data/spec/statesman/machine_spec.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f687db42dd1b0fc5cd988d7ec0ebcab007987a15
|
4
|
+
data.tar.gz: 659f1fd7fe54809344ef91627df309ea7aa5765b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f3f81abd3724a20f36f89c518fcaec9058c65571cd49b297d24ab695e1808f1da5b4e6e92ab52177915d95ba54ae13bc014582c7958085b0ba527c82b96a79fa
|
7
|
+
data.tar.gz: a437668bf5b79bd9671f0bd0387ac44ceac25f7767c9204ce78a8338fc6d5117d9afb0620d69f569bb20e5025b4962a31290761a5fcebbdad7d82c0ef4f54bed
|
data/.rubocop.yml
CHANGED
data/.travis.yml
CHANGED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
## v0.2.0, 16 December 2013
|
2
|
+
*Additions*
|
3
|
+
- Adds Ruby 1.9.3 support (patch by [@jakehow](https://github.com/jakehow))
|
4
|
+
- All Mongo dependent tests are tagged so they can be excluded from test runs
|
5
|
+
|
6
|
+
*Changes*
|
7
|
+
- Specs now crash immediately if Mongo is not running
|
8
|
+
|
9
|
+
## v0.1.0, 5 November 2013
|
10
|
+
|
11
|
+
*Additions*
|
12
|
+
- Adds Mongoid adapter and generators (patch by [@dluxemburg](https://github.com/dluxemburg))
|
13
|
+
|
14
|
+
*Changes*
|
15
|
+
- Replaces `config#transition_class` with `Statesman::Adapters::ActiveRecordTransition` mixin. (inspired by [@cjbell88](https://github.com/cjbell88))
|
16
|
+
- Renames the active record transition generator from `statesman:transition` to `statesman:active_record_transition`.
|
17
|
+
- Moves to using `require_relative` internally where possible to avoid stomping on application load paths.
|
18
|
+
|
19
|
+
## v0.0.1, 28 October 2013.
|
20
|
+
- Initial release
|
data/README.md
CHANGED
@@ -1,14 +1,20 @@
|
|
1
1
|

|
2
2
|
|
3
|
-
A statesmanlike state machine library for Ruby 2.0.
|
3
|
+
A statesmanlike state machine library for Ruby 1.9.3 and 2.0.
|
4
4
|
|
5
5
|
[](http://badge.fury.io/rb/statesman)
|
6
6
|
[](https://travis-ci.org/gocardless/statesman)
|
7
7
|
[](https://codeclimate.com/github/gocardless/statesman)
|
8
8
|
|
9
|
-
Statesman is a little different from other state machine libraries which tack
|
9
|
+
Statesman is a little different from other state machine libraries which tack
|
10
|
+
state behaviour directly onto a model. A statesman state machine is defined as a
|
11
|
+
separate class which is instantiated with the model to which it should
|
12
|
+
apply. State transitions are also modelled as a class which can optionally be
|
13
|
+
persisted to the database for a full audit history, including JSON metadata
|
14
|
+
which can be set during a transition.
|
10
15
|
|
11
|
-
This data model allows for interesting things like using a different state
|
16
|
+
This data model allows for interesting things like using a different state
|
17
|
+
machine depending on the value of a model attribute.
|
12
18
|
|
13
19
|
## TL;DR Usage
|
14
20
|
|
@@ -24,7 +30,7 @@ class OrderStateMachine
|
|
24
30
|
state :failed
|
25
31
|
state :refunded
|
26
32
|
|
27
|
-
transition from: :
|
33
|
+
transition from: :pending, to: [:checking_out, :cancelled]
|
28
34
|
transition from: :checking_out, to: [:purchased, :cancelled]
|
29
35
|
transition from: :purchased, to: [:shipped, :failed]
|
30
36
|
transition from: :shipped, to: :refunded
|
@@ -61,7 +67,7 @@ class OrderTransition < ActiveRecord::Base
|
|
61
67
|
end
|
62
68
|
|
63
69
|
Order.first.state_machine.current_state
|
64
|
-
# => "
|
70
|
+
# => "pending"
|
65
71
|
|
66
72
|
Order.first.state_machine.can_transition_to?(:cancelled)
|
67
73
|
# => true/false
|
@@ -78,7 +84,7 @@ Order.first.state_machine.transition_to!(:cancelled)
|
|
78
84
|
By default Statesman stores transition history in memory only. It can be
|
79
85
|
persisted by configuring Statesman to use a different adapter. For example,
|
80
86
|
ActiveRecord within Rails:
|
81
|
-
|
87
|
+
|
82
88
|
`config/initializers/statesman.rb`:
|
83
89
|
|
84
90
|
```ruby
|
@@ -123,7 +129,9 @@ Statesman.configure do
|
|
123
129
|
storage_adapter(Statesman::Adapters::Mongoid)
|
124
130
|
end
|
125
131
|
```
|
126
|
-
Statesman defaults to storing transitions in memory. If you're using rails, you
|
132
|
+
Statesman defaults to storing transitions in memory. If you're using rails, you
|
133
|
+
can instead configure it to persist transitions to the database by using the
|
134
|
+
ActiveRecord or Mongoid adapter.
|
127
135
|
|
128
136
|
|
129
137
|
## Class methods
|
@@ -139,7 +147,8 @@ Define a new state and optionally mark as the initial state.
|
|
139
147
|
```ruby
|
140
148
|
Machine.transition(from: :some_state, to: :another_state)
|
141
149
|
```
|
142
|
-
Define a transition rule. Both method parameters are required, `to` can also be
|
150
|
+
Define a transition rule. Both method parameters are required, `to` can also be
|
151
|
+
an array of states (`.transition(from: :some_state, to: [:another_state, :some_other_state])`).
|
143
152
|
|
144
153
|
#### `Machine.guard_transition`
|
145
154
|
```ruby
|
@@ -147,15 +156,19 @@ Machine.guard_transition(from: :some_state, to: another_state) do |object|
|
|
147
156
|
object.some_boolean?
|
148
157
|
end
|
149
158
|
```
|
150
|
-
Define a guard. `to` and `from` parameters are optional, a nil parameter means
|
159
|
+
Define a guard. `to` and `from` parameters are optional, a nil parameter means
|
160
|
+
guard all transitions. The passed block should evaluate to a boolean and must
|
161
|
+
be idempotent as it could be called many times.
|
151
162
|
|
152
163
|
#### `Machine.before_transition`
|
153
164
|
```ruby
|
154
|
-
Machine.before_transition(from: :some_state, to: another_state) do |object|
|
165
|
+
Machine.before_transition(from: :some_state, to: another_state) do |object|
|
155
166
|
object.side_effect
|
156
167
|
end
|
157
168
|
```
|
158
|
-
Define a callback to run before a transition. `to` and `from` parameters are
|
169
|
+
Define a callback to run before a transition. `to` and `from` parameters are
|
170
|
+
optional, a nil parameter means run before all transitions. This callback can
|
171
|
+
have side-effects as it will only be run once immediately before the transition.
|
159
172
|
|
160
173
|
#### `Machine.after_transition`
|
161
174
|
```ruby
|
@@ -163,13 +176,19 @@ Machine.after_transition(from: :some_state, to: another_state) do |object, trans
|
|
163
176
|
object.side_effect
|
164
177
|
end
|
165
178
|
```
|
166
|
-
Define a callback to run after a successful transition. `to` and `from`
|
179
|
+
Define a callback to run after a successful transition. `to` and `from`
|
180
|
+
parameters are optional, a nil parameter means run after all transitions. The
|
181
|
+
model object and transition object are passed as arguments to the callback.
|
182
|
+
This callback can have side-effects as it will only be run once immediately
|
183
|
+
after the transition.
|
167
184
|
|
168
185
|
#### `Machine.new`
|
169
186
|
```ruby
|
170
187
|
my_machine = Machine.new(my_model, transition_class: MyTransitionModel)
|
171
188
|
```
|
172
|
-
Initialize a new state machine instance. `my_model` is required. If using the
|
189
|
+
Initialize a new state machine instance. `my_model` is required. If using the
|
190
|
+
ActiveRecord adapter `my_model` should have a `has_many` association with
|
191
|
+
`MyTransitionModel`.
|
173
192
|
|
174
193
|
## Instance methods
|
175
194
|
|
@@ -183,13 +202,16 @@ Returns a sorted array of all transition objects.
|
|
183
202
|
Returns the most recent transition object.
|
184
203
|
|
185
204
|
#### `Machine#can_transition_to?(:state)`
|
186
|
-
Returns true if the current state can transition to the passed state and all
|
205
|
+
Returns true if the current state can transition to the passed state and all
|
206
|
+
applicable guards pass.
|
187
207
|
|
188
208
|
#### `Machine#transition_to!(:state)`
|
189
|
-
Transition to the passed state, returning `true` on success. Raises
|
209
|
+
Transition to the passed state, returning `true` on success. Raises
|
210
|
+
`Statesman::GuardFailedError` or `Statesman::TransitionFailedError` on failure.
|
190
211
|
|
191
212
|
#### `Machine#transition_to(:state)`
|
192
|
-
Transition to the passed state, returning `true` on success. Swallows all
|
213
|
+
Transition to the passed state, returning `true` on success. Swallows all
|
214
|
+
exceptions and returns false on failure.
|
193
215
|
|
194
216
|
---
|
195
217
|
|
data/lib/statesman/callback.rb
CHANGED
@@ -6,21 +6,23 @@ module Statesman
|
|
6
6
|
attr_reader :to
|
7
7
|
attr_reader :callback
|
8
8
|
|
9
|
-
def initialize(from: nil, to: nil, callback: nil)
|
10
|
-
unless callback.respond_to?(:call)
|
9
|
+
def initialize(options = { from: nil, to: nil, callback: nil })
|
10
|
+
unless options[:callback].respond_to?(:call)
|
11
11
|
raise InvalidCallbackError, "No callback passed"
|
12
12
|
end
|
13
13
|
|
14
|
-
@from = from
|
15
|
-
@to = to
|
16
|
-
@callback = callback
|
14
|
+
@from = options[:from]
|
15
|
+
@to = options[:to]
|
16
|
+
@callback = options[:callback]
|
17
17
|
end
|
18
18
|
|
19
19
|
def call(*args)
|
20
20
|
callback.call(*args)
|
21
21
|
end
|
22
22
|
|
23
|
-
def applies_to?(from: nil, to: nil)
|
23
|
+
def applies_to?(options = { from: nil, to: nil })
|
24
|
+
from = options[:from]
|
25
|
+
to = options[:to]
|
24
26
|
# rubocop:disable RedundantSelf
|
25
27
|
(self.from.nil? && self.to.nil?) ||
|
26
28
|
(from.nil? && to == self.to) ||
|
data/lib/statesman/machine.rb
CHANGED
@@ -19,9 +19,9 @@ module Statesman
|
|
19
19
|
@states ||= []
|
20
20
|
end
|
21
21
|
|
22
|
-
def state(name, initial: false)
|
22
|
+
def state(name, options = { initial: false })
|
23
23
|
name = name.to_s
|
24
|
-
if initial
|
24
|
+
if options[:initial]
|
25
25
|
validate_initial_state(name)
|
26
26
|
@initial_state = name
|
27
27
|
end
|
@@ -44,9 +44,9 @@ module Statesman
|
|
44
44
|
@guards ||= []
|
45
45
|
end
|
46
46
|
|
47
|
-
def transition(from: nil, to: nil)
|
48
|
-
from = to_s_or_nil(from)
|
49
|
-
to = Array(to).map { |item| to_s_or_nil(item) }
|
47
|
+
def transition(options = { from: nil, to: nil })
|
48
|
+
from = to_s_or_nil(options[:from])
|
49
|
+
to = Array(options[:to]).map { |item| to_s_or_nil(item) }
|
50
50
|
|
51
51
|
successors[from] ||= []
|
52
52
|
|
@@ -55,33 +55,33 @@ module Statesman
|
|
55
55
|
successors[from] += to
|
56
56
|
end
|
57
57
|
|
58
|
-
def before_transition(from: nil, to: nil, &block)
|
59
|
-
from = to_s_or_nil(from)
|
60
|
-
to = to_s_or_nil(to)
|
58
|
+
def before_transition(options = { from: nil, to: nil }, &block)
|
59
|
+
from = to_s_or_nil(options[:from])
|
60
|
+
to = to_s_or_nil(options[:to])
|
61
61
|
|
62
62
|
validate_callback_condition(from: from, to: to)
|
63
63
|
before_callbacks << Callback.new(from: from, to: to, callback: block)
|
64
64
|
end
|
65
65
|
|
66
|
-
def after_transition(from: nil, to: nil, &block)
|
67
|
-
from = to_s_or_nil(from)
|
68
|
-
to = to_s_or_nil(to)
|
66
|
+
def after_transition(options = { from: nil, to: nil }, &block)
|
67
|
+
from = to_s_or_nil(options[:from])
|
68
|
+
to = to_s_or_nil(options[:to])
|
69
69
|
|
70
70
|
validate_callback_condition(from: from, to: to)
|
71
71
|
after_callbacks << Callback.new(from: from, to: to, callback: block)
|
72
72
|
end
|
73
73
|
|
74
|
-
def guard_transition(from: nil, to: nil, &block)
|
75
|
-
from = to_s_or_nil(from)
|
76
|
-
to = to_s_or_nil(to)
|
74
|
+
def guard_transition(options = { from: nil, to: nil }, &block)
|
75
|
+
from = to_s_or_nil(options[:from])
|
76
|
+
to = to_s_or_nil(options[:to])
|
77
77
|
|
78
78
|
validate_callback_condition(from: from, to: to)
|
79
79
|
guards << Guard.new(from: from, to: to, callback: block)
|
80
80
|
end
|
81
81
|
|
82
|
-
def validate_callback_condition(from: nil, to: nil)
|
83
|
-
from = to_s_or_nil(from)
|
84
|
-
to = to_s_or_nil(to)
|
82
|
+
def validate_callback_condition(options = { from: nil, to: nil })
|
83
|
+
from = to_s_or_nil(options[:from])
|
84
|
+
to = to_s_or_nil(options[:to])
|
85
85
|
|
86
86
|
[from, to].compact.each { |state| validate_state(state) }
|
87
87
|
return if from.nil? && to.nil?
|
@@ -139,10 +139,12 @@ module Statesman
|
|
139
139
|
end
|
140
140
|
|
141
141
|
def initialize(object,
|
142
|
-
|
142
|
+
options = {
|
143
|
+
transition_class: Statesman::Adapters::MemoryTransition
|
144
|
+
})
|
143
145
|
@object = object
|
144
|
-
@storage_adapter = Statesman.storage_adapter.new(
|
145
|
-
|
146
|
+
@storage_adapter = Statesman.storage_adapter.new(
|
147
|
+
options[:transition_class], object)
|
146
148
|
end
|
147
149
|
|
148
150
|
def current_state
|
@@ -191,31 +193,31 @@ module Statesman
|
|
191
193
|
|
192
194
|
private
|
193
195
|
|
194
|
-
def guards_for(from: nil, to: nil)
|
195
|
-
select_callbacks_for(self.class.guards,
|
196
|
+
def guards_for(options = { from: nil, to: nil })
|
197
|
+
select_callbacks_for(self.class.guards, options)
|
196
198
|
end
|
197
199
|
|
198
|
-
def before_callbacks_for(from: nil, to: nil)
|
199
|
-
select_callbacks_for(self.class.before_callbacks,
|
200
|
+
def before_callbacks_for(options = { from: nil, to: nil })
|
201
|
+
select_callbacks_for(self.class.before_callbacks, options)
|
200
202
|
end
|
201
203
|
|
202
|
-
def after_callbacks_for(from: nil, to: nil)
|
203
|
-
select_callbacks_for(self.class.after_callbacks,
|
204
|
+
def after_callbacks_for(options = { from: nil, to: nil })
|
205
|
+
select_callbacks_for(self.class.after_callbacks, options)
|
204
206
|
end
|
205
207
|
|
206
|
-
def select_callbacks_for(callbacks, from: nil, to: nil)
|
207
|
-
from = to_s_or_nil(from)
|
208
|
-
to = to_s_or_nil(to)
|
208
|
+
def select_callbacks_for(callbacks, options = { from: nil, to: nil })
|
209
|
+
from = to_s_or_nil(options[:from])
|
210
|
+
to = to_s_or_nil(options[:to])
|
209
211
|
callbacks.select { |callback| callback.applies_to?(from: from, to: to) }
|
210
212
|
end
|
211
213
|
|
212
|
-
def validate_transition(from: nil, to: nil, metadata: nil)
|
213
|
-
from = to_s_or_nil(from)
|
214
|
-
to = to_s_or_nil(to)
|
214
|
+
def validate_transition(options = { from: nil, to: nil, metadata: nil })
|
215
|
+
from = to_s_or_nil(options[:from])
|
216
|
+
to = to_s_or_nil(options[:to])
|
215
217
|
|
216
218
|
# Call all guards, they raise exceptions if they fail
|
217
219
|
guards_for(from: from, to: to).each do |guard|
|
218
|
-
guard.call(@object, last_transition, metadata)
|
220
|
+
guard.call(@object, last_transition, options[:metadata])
|
219
221
|
end
|
220
222
|
|
221
223
|
successors = self.class.successors[from] || []
|
data/lib/statesman/version.rb
CHANGED
data/spec/spec_helper.rb
CHANGED
@@ -2,13 +2,29 @@ require "statesman"
|
|
2
2
|
require "sqlite3"
|
3
3
|
require "active_record"
|
4
4
|
require "support/active_record"
|
5
|
+
require "mongoid"
|
5
6
|
|
6
7
|
RSpec.configure do |config|
|
7
8
|
config.expect_with :rspec do |c|
|
8
9
|
c.syntax = :expect
|
9
10
|
end
|
10
11
|
|
11
|
-
config.order =
|
12
|
+
config.order = "random"
|
13
|
+
|
14
|
+
# Try a mongo connection at the start of the suite and raise if it fails
|
15
|
+
begin
|
16
|
+
Mongoid.configure do |mongo_config|
|
17
|
+
mongo_config.connect_to("statesman_test")
|
18
|
+
mongo_config.sessions["default"]["options"]["max_retries"] = 2
|
19
|
+
end
|
20
|
+
# Attempting a mongo operation will trigger 2 retries then throw an
|
21
|
+
# exception if mongo is not running.
|
22
|
+
Mongoid.purge! unless config.exclusion_filter[:mongo]
|
23
|
+
rescue Moped::Errors::ConnectionFailure => error
|
24
|
+
puts "The spec suite requires MongoDB to be installed and running locally"
|
25
|
+
puts "Mongo dependent specs can be filtered with rspec --tag '~mongo'"
|
26
|
+
raise(error)
|
27
|
+
end
|
12
28
|
|
13
29
|
config.before(:each) do
|
14
30
|
# Connect to & cleanup test database
|
@@ -331,7 +331,7 @@ describe Statesman::Machine do
|
|
331
331
|
|
332
332
|
context "with a guard" do
|
333
333
|
let(:result) { true }
|
334
|
-
let(:guard_cb) { ->
|
334
|
+
let(:guard_cb) { ->(*args) { result } }
|
335
335
|
before { machine.guard_transition(from: :x, to: :y, &guard_cb) }
|
336
336
|
|
337
337
|
context "and an object to act on" do
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: statesman
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Harry Marr
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-
|
12
|
+
date: 2013-12-16 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|
@@ -147,6 +147,7 @@ files:
|
|
147
147
|
- .gitignore
|
148
148
|
- .rubocop.yml
|
149
149
|
- .travis.yml
|
150
|
+
- CHANGELOG.md
|
150
151
|
- Gemfile
|
151
152
|
- Guardfile
|
152
153
|
- LICENSE.txt
|