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 CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- stately (0.0.1)
4
+ stately (0.2.0)
5
5
 
6
6
  GEM
7
7
  remote: http://rubygems.org/
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Stately
2
2
 
3
- An elegant state machine for your ruby objects.
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
- Class Order do
12
- stately start: :processing do
13
- state :completed do
14
- prevent_from :refunded
11
+ ```ruby
12
+ class Order
13
+ stately start: :processing do
14
+ state :completed do
15
+ prevent_from :refunded
15
16
 
16
- before_transition from: :processing, do: :calculate_total
17
- after_transition do: :email_receipt
17
+ before_transition from: :processing, do: :calculate_total
18
+ after_transition do: :email_receipt
18
19
 
19
- validate :validates_credit_card
20
- end
20
+ validate :validates_credit_card
21
+ end
21
22
 
22
- state :invalid do
23
- prevent_from :completed, :refunded
24
- end
23
+ state :invalid do
24
+ prevent_from :completed, :refunded
25
+ end
25
26
 
26
- state :refunded do
27
- allow_from :completed
27
+ state :refunded do
28
+ allow_from :completed
28
29
 
29
- after_transition do: :email_receipt
30
- end
31
- end
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
- gem install stately
59
+ ```shell
60
+ gem install stately
61
+ ```
40
62
 
41
63
  or add it to your Gemfile:
42
64
 
43
- gem stately
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
- stately start: :initial_state, attr: :my_state_attr do
50
- # ...
51
- end
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
- Class Order
60
- stately start: :processing do
61
- state :my_state, action: transition_to_my_state
62
- end
63
- end
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
- order = Order.new
66
- order.transition_to_my_state
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
- state :completed do
73
- allow_from :processing
74
- prevent_from :refunded
75
- end
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
- state :completed do
84
- validate :validates_amount
85
- validate :validates_credit_card
86
- end
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 a database, you'll almost always want an `after_transition` that calls `save` or the equivalent.
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+. If you'd like to contribute to Stately, you'll need Rspec 2.0+.
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
 
@@ -1,5 +1,5 @@
1
1
  # Includes Stately on Ruby's Object.
2
2
 
3
3
  Object.class_eval do
4
- include Stately
4
+ include Stately::Core
5
5
  end
@@ -1,3 +1,3 @@
1
1
  module Stately
2
- VERSION = '0.1.0'
2
+ VERSION = '0.2.0'
3
3
  end
data/lib/stately.rb CHANGED
@@ -7,75 +7,67 @@ module Stately
7
7
  class InvalidTransition < StandardError
8
8
  end
9
9
 
10
- # Define a new Stately state machine.
11
- #
12
- # As an example, let's say you have an Order object and you'd like an elegant state machine for
13
- # it. Here's one way you might set it up:
14
- #
15
- # Class Order do
16
- # stately start: :processing do
17
- # state :completed do
18
- # prevent_from :refunded
19
- #
20
- # before_transition from: :processing, do: :calculate_total
21
- # after_transition do: :email_receipt
22
- #
23
- # validate :validates_credit_card
24
- # end
25
- #
26
- # state :invalid do
27
- # prevent_from :completed, :refunded
28
- # end
29
- #
30
- # state :refunded do
31
- # allow_from :completed
32
- #
33
- # after_transition do: :email_receipt
34
- # end
35
- # end
36
- # end
37
- #
38
- # This example is doing quite a few things, paraphrased as:
39
- #
40
- # * It sets up a new state machine using the default state attribute on Order to store the
41
- # current state. It also indicates the initial state should be :processing.
42
- # * It defines three states: :completed, :refunded, and :invalid
43
- # * Order can transition to the completed state from all but the refunded state. Similar
44
- # definitions are setup for the other two states.
45
- # * Callbacks are setup using before_transition and after_transition
46
- # * Validations are added. If a validation fails, it prevents the transition.
47
- #
48
- # Stately tries hard not to surprise you. In a typical Stately implementation, you'll always have
49
- # an after_transition, primarily to call save (or whatever the equivalent is to store the
50
- # instance's current state).
51
- def stately(*opts, &block)
52
- options = opts.last.is_a?(Hash) ? opts.last : {}
53
- options[:attr] ||= :state
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
- # Get the current Stately::Machine object
62
- def self.stately_machine
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
- # Get the current Stately::Machine object
67
- def stately_machine
68
- @@stately_machine
69
- end
59
+ include Stately::InstanceMethods
60
+ end
70
61
 
71
- # Set the current Stately::Machine object
72
- def self.stately_machine=(obj)
73
- @@stately_machine = obj
74
- end
62
+ # Get the current Stately::Machine object
63
+ def stately_machine
64
+ @@stately_machine
65
+ end
75
66
 
76
- # Set the current Stately::Machine object
77
- def stately_machine=(obj)
78
- @@stately_machine = obj
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.1.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: 2012-11-03 00:00:00.000000000 Z
12
+ date: 2013-01-21 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: redcarpet