stately 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile.lock +1 -1
- data/README.md +103 -39
- data/lib/stately/core_ext.rb +1 -1
- data/lib/stately/version.rb +1 -1
- data/lib/stately.rb +57 -65
- metadata +2 -2
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Stately
|
2
2
|
|
3
|
-
|
3
|
+
A minimal, elegant state machine for your ruby objects.
|
4
4
|
|
5
5
|
![A stately fellow.](https://dl.dropbox.com/u/2754528/exquisite_cat.jpg "A stately fellow.")
|
6
6
|
|
@@ -8,47 +8,73 @@ An elegant state machine for your ruby objects.
|
|
8
8
|
|
9
9
|
Stately is a state machine for ruby objects, with an elegant, easy-to-read DSL. Here's an example showing off what Stately can do:
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
11
|
+
```ruby
|
12
|
+
class Order
|
13
|
+
stately start: :processing do
|
14
|
+
state :completed do
|
15
|
+
prevent_from :refunded
|
15
16
|
|
16
|
-
|
17
|
-
|
17
|
+
before_transition from: :processing, do: :calculate_total
|
18
|
+
after_transition do: :email_receipt
|
18
19
|
|
19
|
-
|
20
|
-
|
20
|
+
validate :validates_credit_card
|
21
|
+
end
|
21
22
|
|
22
|
-
|
23
|
-
|
24
|
-
|
23
|
+
state :invalid do
|
24
|
+
prevent_from :completed, :refunded
|
25
|
+
end
|
25
26
|
|
26
|
-
|
27
|
-
|
27
|
+
state :refunded do
|
28
|
+
allow_from :completed
|
28
29
|
|
29
|
-
|
30
|
-
|
31
|
-
|
30
|
+
after_transition do: :email_receipt
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
```
|
32
35
|
|
33
36
|
Stately tries hard not to surprise you. When you transition to a new state, you're responsible for taking whatever actions that means using `before_transition` and `after_transition`. Stately also has no dependencies on things like DataMapper or ActiveModel, so it will never surprise you with an implicit `save` after transitioning states.
|
34
37
|
|
38
|
+
## When to use stately
|
39
|
+
|
40
|
+
Often, you'll find yourself writing an object that can have multiple states. Tracking these states can usually be done either:
|
41
|
+
|
42
|
+
* By hand (i.e. adding a string column in the db and storing the current state there).
|
43
|
+
* Via a state machine of some kind. [state_machine](https://github.com/pluginaweek/state_machine) is a popular one that I've used quite a bit, which has a lot of advanced features (most of which I've never used).
|
44
|
+
|
45
|
+
Stately exists in a middle space between the two options. The goal of stately is to make the most common case, where you just need to track state and react appropriately when switching those states, easy.
|
46
|
+
|
47
|
+
## Design goals
|
48
|
+
|
49
|
+
* Minimalist. Stately tries to solve the most common use case: tracking the current state and handling transitions between states.
|
50
|
+
|
51
|
+
* No magic. In other words, if you're using, say, ActiveRecord, stately won't hook in to activerecord callbacks. This requires you to be more explicit and perhaps more verbose, but I think it helps with readability and reduces surprises. See the Examples section below for what this looks like when in an ActiveRecord environment.
|
52
|
+
|
53
|
+
* Syntax that is as self-documenting as possible. Someone not familiar with Stately should be able to understand what happens when an object's state is changed just by reading the DSL.
|
54
|
+
|
35
55
|
## Getting started
|
36
56
|
|
37
57
|
Either install locally:
|
38
58
|
|
39
|
-
|
59
|
+
```shell
|
60
|
+
gem install stately
|
61
|
+
```
|
40
62
|
|
41
63
|
or add it to your Gemfile:
|
42
64
|
|
43
|
-
|
65
|
+
```ruby
|
66
|
+
gem stately
|
67
|
+
```
|
44
68
|
|
45
69
|
Be sure to run `bundle install` afterwards.
|
46
70
|
|
47
71
|
The first step is to add the following to your object:
|
48
72
|
|
49
|
-
|
50
|
-
|
51
|
-
|
73
|
+
```ruby
|
74
|
+
stately start: :initial_state, attr: :my_state_attr do
|
75
|
+
# ...
|
76
|
+
end
|
77
|
+
```
|
52
78
|
|
53
79
|
This sets up Stately to look for an attribute named `my_state_attr`, and initially set it to `initial_state`. If you omit `attr: :my_state_attr`, Stately will automatically look for an attribute named `state`.
|
54
80
|
|
@@ -56,23 +82,27 @@ This sets up Stately to look for an attribute named `my_state_attr`, and initial
|
|
56
82
|
|
57
83
|
States make up the core of Stately and define two things: the name of the state (i.e. "completed"), and a verb as the name of the method to call to begin a transition into that state (i.e. "complete"). Stately has support for some common state/verb combinations, but you can always use your own:
|
58
84
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
85
|
+
```ruby
|
86
|
+
class Order
|
87
|
+
stately start: :processing do
|
88
|
+
state :my_state, action: transition_to_my_state
|
89
|
+
end
|
90
|
+
end
|
64
91
|
|
65
|
-
|
66
|
-
|
92
|
+
order = Order.new
|
93
|
+
order.transition_to_my_state
|
94
|
+
```
|
67
95
|
|
68
96
|
## Transitions
|
69
97
|
|
70
98
|
A "transition" is the process of moving from one state to another. You can define legal transitions using `allow_from` and `prevent_from`:
|
71
99
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
100
|
+
```ruby
|
101
|
+
state :completed do
|
102
|
+
allow_from :processing
|
103
|
+
prevent_from :refunded
|
104
|
+
end
|
105
|
+
```
|
76
106
|
|
77
107
|
In the above example, if you try to transition to `completed` (by calling `complete` on the object) from `refunded`, you'll see a `Stately::InvalidTransition` is raised. By default, all transitions are allowed.
|
78
108
|
|
@@ -80,10 +110,12 @@ In the above example, if you try to transition to `completed` (by calling `compl
|
|
80
110
|
|
81
111
|
While transitioning from one state to another, you can define validations to be run. If any validation returns `false`, the transition is halted.
|
82
112
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
113
|
+
```ruby
|
114
|
+
state :completed do
|
115
|
+
validate :validates_amount
|
116
|
+
validate :validates_credit_card
|
117
|
+
end
|
118
|
+
```
|
87
119
|
|
88
120
|
Each validation is also called in order, so first `validates_amount` will be called, and if it doesn't return `false`, then `validates_credit_card` will be called and checked.
|
89
121
|
|
@@ -91,21 +123,53 @@ Each validation is also called in order, so first `validates_amount` will be cal
|
|
91
123
|
|
92
124
|
Callbacks can be defined to run either before or after a transition occurs. A `before_transition` is run after validations are checked, but before the `state_attr` has been written to with the new state. An `after_transition` is called after the `state_attr` has been written to.
|
93
125
|
|
94
|
-
If you're using Stately with
|
126
|
+
If you're using Stately with some kind of persistence layer, sych as activerecord, you'll probably want an `after_transition` that calls `save` or the equivalent.
|
127
|
+
|
128
|
+
```ruby
|
129
|
+
class Order
|
130
|
+
stately start: :processing do
|
131
|
+
# ...
|
95
132
|
|
96
133
|
state :completed do
|
97
134
|
before_transition from: :processing, do: :before_completed
|
98
135
|
before_transition from: :invalid, do: :cleanup_invalid
|
99
136
|
after_transition do: :after_completed
|
100
137
|
end
|
138
|
+
end
|
139
|
+
|
140
|
+
private
|
141
|
+
|
142
|
+
def after_completed
|
143
|
+
save
|
144
|
+
end
|
145
|
+
end
|
146
|
+
```
|
101
147
|
|
102
148
|
A callback can include an optional `from` state name, which is only called when transitioning from the named state. Omitting it means the callback is always called.
|
103
149
|
|
104
150
|
Additionally, each callback is executed in the order in which it's defined.
|
105
151
|
|
152
|
+
## Example: using Stately with ActiveRecord
|
153
|
+
|
154
|
+
Let's say you are modeling a Bicycle object for your rental shop and you're using ActiveRecord. A Bicycle has two states: `available` and `rented`. Using stately, you could define this as the following:
|
155
|
+
|
156
|
+
```ruby
|
157
|
+
class Bicycle < ActiveRecord::Base
|
158
|
+
stately start: :available do
|
159
|
+
state :rented, action: :rent do
|
160
|
+
after_transition do: :save
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
```
|
165
|
+
|
166
|
+
When Bicycle is first instantiated, its `state` column is set to the string `available`. If you want to rent the Bicycle, you'd call `bicycle.rent`, which would update the `state` column to be the string `rented` and then call the ActiveRecord method `save`.
|
167
|
+
|
168
|
+
As you can see, Stately is slightly more verbose than other state machine gems, but with the upside of being more self-documenting. Additionally, it doesn't hook into ActiveRecord's callback chains, and instead requires you to explicitely call `save`.
|
169
|
+
|
106
170
|
## Requirements
|
107
171
|
|
108
|
-
Stately requires Ruby 1.9
|
172
|
+
Stately requires Ruby 1.9. If you'd like to contribute to Stately, you'll need Rspec 2.0+.
|
109
173
|
|
110
174
|
## License
|
111
175
|
|
data/lib/stately/core_ext.rb
CHANGED
data/lib/stately/version.rb
CHANGED
data/lib/stately.rb
CHANGED
@@ -7,75 +7,67 @@ module Stately
|
|
7
7
|
class InvalidTransition < StandardError
|
8
8
|
end
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
self.stately_machine = Stately::Machine.new(options[:attr], options[:start])
|
56
|
-
self.stately_machine.instance_eval(&block) if block_given?
|
57
|
-
|
58
|
-
include Stately::InstanceMethods
|
59
|
-
end
|
10
|
+
module Core
|
11
|
+
# Define a new Stately state machine.
|
12
|
+
#
|
13
|
+
# As an example, let's say you have an Order object and you'd like an elegant state machine for
|
14
|
+
# it. Here's one way you might set it up:
|
15
|
+
#
|
16
|
+
# Class Order do
|
17
|
+
# stately start: :processing do
|
18
|
+
# state :completed do
|
19
|
+
# prevent_from :refunded
|
20
|
+
#
|
21
|
+
# before_transition from: :processing, do: :calculate_total
|
22
|
+
# after_transition do: :email_receipt
|
23
|
+
#
|
24
|
+
# validate :validates_credit_card
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# state :invalid do
|
28
|
+
# prevent_from :completed, :refunded
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# state :refunded do
|
32
|
+
# allow_from :completed
|
33
|
+
#
|
34
|
+
# after_transition do: :email_receipt
|
35
|
+
# end
|
36
|
+
# end
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# This example is doing quite a few things, paraphrased as:
|
40
|
+
#
|
41
|
+
# * It sets up a new state machine using the default state attribute on Order to store the
|
42
|
+
# current state. It also indicates the initial state should be :processing.
|
43
|
+
# * It defines three states: :completed, :refunded, and :invalid
|
44
|
+
# * Order can transition to the completed state from all but the refunded state. Similar
|
45
|
+
# definitions are setup for the other two states.
|
46
|
+
# * Callbacks are setup using before_transition and after_transition
|
47
|
+
# * Validations are added. If a validation fails, it prevents the transition.
|
48
|
+
#
|
49
|
+
# Stately tries hard not to surprise you. In a typical Stately implementation, you'll always have
|
50
|
+
# an after_transition, primarily to call save (or whatever the equivalent is to store the
|
51
|
+
# instance's current state).
|
52
|
+
def stately(*opts, &block)
|
53
|
+
options = opts.last.is_a?(Hash) ? opts.last : {}
|
54
|
+
options[:attr] ||= :state
|
60
55
|
|
61
|
-
|
62
|
-
|
63
|
-
@@stately_machine
|
64
|
-
end
|
56
|
+
self.stately_machine = Stately::Machine.new(options[:attr], options[:start])
|
57
|
+
self.stately_machine.instance_eval(&block) if block_given?
|
65
58
|
|
66
|
-
|
67
|
-
|
68
|
-
@@stately_machine
|
69
|
-
end
|
59
|
+
include Stately::InstanceMethods
|
60
|
+
end
|
70
61
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
62
|
+
# Get the current Stately::Machine object
|
63
|
+
def stately_machine
|
64
|
+
@@stately_machine
|
65
|
+
end
|
75
66
|
|
76
|
-
|
77
|
-
|
78
|
-
|
67
|
+
# Set the current Stately::Machine object
|
68
|
+
def stately_machine=(obj)
|
69
|
+
@@stately_machine = obj
|
70
|
+
end
|
79
71
|
end
|
80
72
|
|
81
73
|
module InstanceMethods
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: stately
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2013-01-21 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: redcarpet
|