stately 0.1.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.
- data/.gitignore +11 -0
- data/.rspec +2 -0
- data/.yardopts +4 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +28 -0
- data/MIT-LICENSE +20 -0
- data/README.md +112 -0
- data/Rakefile +38 -0
- data/lib/stately/core_ext.rb +5 -0
- data/lib/stately/machine.rb +26 -0
- data/lib/stately/state.rb +96 -0
- data/lib/stately/version.rb +3 -0
- data/lib/stately.rb +204 -0
- data/lib/tasks/stately_tasks.rake +4 -0
- data/spec/functional/stately_spec.rb +305 -0
- data/spec/spec_helper.rb +5 -0
- data/spec/unit/stately/machine_spec.rb +119 -0
- data/spec/unit/stately/state_spec.rb +101 -0
- data/spec/unit/stately_spec.rb +58 -0
- data/stately.gemspec +22 -0
- metadata +114 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/.yardopts
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
stately (0.0.1)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: http://rubygems.org/
|
8
|
+
specs:
|
9
|
+
diff-lcs (1.1.3)
|
10
|
+
redcarpet (2.2.2)
|
11
|
+
rspec (2.11.0)
|
12
|
+
rspec-core (~> 2.11.0)
|
13
|
+
rspec-expectations (~> 2.11.0)
|
14
|
+
rspec-mocks (~> 2.11.0)
|
15
|
+
rspec-core (2.11.1)
|
16
|
+
rspec-expectations (2.11.3)
|
17
|
+
diff-lcs (~> 1.1.3)
|
18
|
+
rspec-mocks (2.11.3)
|
19
|
+
yard (0.8.3)
|
20
|
+
|
21
|
+
PLATFORMS
|
22
|
+
ruby
|
23
|
+
|
24
|
+
DEPENDENCIES
|
25
|
+
redcarpet (~> 2.2.2)
|
26
|
+
rspec (~> 2.0)
|
27
|
+
stately!
|
28
|
+
yard (~> 0.8.3)
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2012 Ryan Twomey
|
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,112 @@
|
|
1
|
+
# Stately
|
2
|
+
|
3
|
+
An elegant state machine for your ruby objects.
|
4
|
+
|
5
|
+

|
6
|
+
|
7
|
+
## Making a stately start
|
8
|
+
|
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
|
+
|
11
|
+
Class Order do
|
12
|
+
stately start: :processing do
|
13
|
+
state :completed do
|
14
|
+
prevent_from :refunded
|
15
|
+
|
16
|
+
before_transition from: :processing, do: :calculate_total
|
17
|
+
after_transition do: :email_receipt
|
18
|
+
|
19
|
+
validate :validates_credit_card
|
20
|
+
end
|
21
|
+
|
22
|
+
state :invalid do
|
23
|
+
prevent_from :completed, :refunded
|
24
|
+
end
|
25
|
+
|
26
|
+
state :refunded do
|
27
|
+
allow_from :completed
|
28
|
+
|
29
|
+
after_transition do: :email_receipt
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
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
|
+
|
35
|
+
## Getting started
|
36
|
+
|
37
|
+
Either install locally:
|
38
|
+
|
39
|
+
gem install stately
|
40
|
+
|
41
|
+
or add it to your Gemfile:
|
42
|
+
|
43
|
+
gem stately
|
44
|
+
|
45
|
+
Be sure to run `bundle install` afterwards.
|
46
|
+
|
47
|
+
The first step is to add the following to your object:
|
48
|
+
|
49
|
+
stately start: :initial_state, attr: :my_state_attr do
|
50
|
+
# ...
|
51
|
+
end
|
52
|
+
|
53
|
+
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
|
+
|
55
|
+
## Defining a state
|
56
|
+
|
57
|
+
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
|
+
|
59
|
+
Class Order
|
60
|
+
stately start: :processing do
|
61
|
+
state :my_state, action: transition_to_my_state
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
order = Order.new
|
66
|
+
order.transition_to_my_state
|
67
|
+
|
68
|
+
## Transitions
|
69
|
+
|
70
|
+
A "transition" is the process of moving from one state to another. You can define legal transitions using `allow_from` and `prevent_from`:
|
71
|
+
|
72
|
+
state :completed do
|
73
|
+
allow_from :processing
|
74
|
+
prevent_from :refunded
|
75
|
+
end
|
76
|
+
|
77
|
+
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
|
+
|
79
|
+
## Validations
|
80
|
+
|
81
|
+
While transitioning from one state to another, you can define validations to be run. If any validation returns `false`, the transition is halted.
|
82
|
+
|
83
|
+
state :completed do
|
84
|
+
validate :validates_amount
|
85
|
+
validate :validates_credit_card
|
86
|
+
end
|
87
|
+
|
88
|
+
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
|
+
|
90
|
+
## Callbacks
|
91
|
+
|
92
|
+
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
|
+
|
94
|
+
If you're using Stately with a database, you'll almost always want an `after_transition` that calls `save` or the equivalent.
|
95
|
+
|
96
|
+
state :completed do
|
97
|
+
before_transition from: :processing, do: :before_completed
|
98
|
+
before_transition from: :invalid, do: :cleanup_invalid
|
99
|
+
after_transition do: :after_completed
|
100
|
+
end
|
101
|
+
|
102
|
+
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
|
+
|
104
|
+
Additionally, each callback is executed in the order in which it's defined.
|
105
|
+
|
106
|
+
## Requirements
|
107
|
+
|
108
|
+
Stately requires Ruby 1.9+. If you'd like to contribute to Stately, you'll need Rspec 2.0+.
|
109
|
+
|
110
|
+
## License
|
111
|
+
|
112
|
+
Stately is Copyright © 2012 Ryan Twomey. It is free software, and may be redistributed under the terms specified in the MIT-LICENSE file.
|
data/Rakefile
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
|
3
|
+
require 'bundler'
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
|
6
|
+
Bundler::GemHelper.install_tasks
|
7
|
+
|
8
|
+
begin
|
9
|
+
require 'rdoc/task'
|
10
|
+
rescue LoadError
|
11
|
+
require 'rdoc/rdoc'
|
12
|
+
require 'rake/rdoctask'
|
13
|
+
RDoc::Task = Rake::RDocTask
|
14
|
+
end
|
15
|
+
|
16
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
17
|
+
rdoc.rdoc_dir = 'rdoc'
|
18
|
+
rdoc.title = 'Stately'
|
19
|
+
rdoc.options << '--line-numbers'
|
20
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
21
|
+
end
|
22
|
+
|
23
|
+
namespace :spec do
|
24
|
+
desc 'Run unit specs'
|
25
|
+
RSpec::Core::RakeTask.new('unit') do |t|
|
26
|
+
t.pattern = 'spec/unit/**/*_spec.rb'
|
27
|
+
end
|
28
|
+
|
29
|
+
desc 'Run functional specs'
|
30
|
+
RSpec::Core::RakeTask.new('functional') do |t|
|
31
|
+
t.pattern = 'spec/functional/**/*_spec.rb'
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
desc 'Run unit and functional specs'
|
36
|
+
task :spec => ['spec:unit', 'spec:functional']
|
37
|
+
|
38
|
+
task :default => :spec
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Stately
|
2
|
+
# A Stately::Machine is a container for Stately::States.
|
3
|
+
class Machine
|
4
|
+
attr_reader :start, :state_attr, :states
|
5
|
+
|
6
|
+
# Sets up a new instance of Stately::Machine
|
7
|
+
def initialize(attr_name, start)
|
8
|
+
@state_attr = attr_name
|
9
|
+
@start = start
|
10
|
+
@states = [State.new(@start)]
|
11
|
+
end
|
12
|
+
|
13
|
+
# Define a new Stately::State and add it to this Stately::Machine.
|
14
|
+
#
|
15
|
+
# @param [String] name The name of the state. This is also stored in the instance object's
|
16
|
+
# state attribute.
|
17
|
+
# @param [Hash] opts Optionally, a method name can be defined as this state's action, if it
|
18
|
+
# can't be inferred from the name.
|
19
|
+
def state(name, opts={}, &block)
|
20
|
+
@states.delete_if { |s| s.name == name }
|
21
|
+
|
22
|
+
action = opts ? opts[:action] : nil
|
23
|
+
@states << State.new(name, action, &block)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module Stately
|
2
|
+
# A Stately::State object contains the configuration and other information about a defined
|
3
|
+
# state.
|
4
|
+
#
|
5
|
+
# It's made up of a name (which is saved to the parent instance's state attribute), the
|
6
|
+
# name of an action (which is a method called to transition into this state), and a DSL to
|
7
|
+
# define allowed transitions, callbacks, and validations.
|
8
|
+
|
9
|
+
class State
|
10
|
+
attr_reader :action, :name
|
11
|
+
attr_reader :allow_from_states, :prevent_from_states
|
12
|
+
attr_reader :after_transitions, :before_transitions, :validations
|
13
|
+
|
14
|
+
# Sets up and returns a new Stately::State object.
|
15
|
+
#
|
16
|
+
# @param [String] name The name of the state
|
17
|
+
# @param [String] action The method name that's called to transition to this state. Some method
|
18
|
+
# names can be inferred based on the state's name.
|
19
|
+
def initialize(name, action=nil, &block)
|
20
|
+
@action = (action || guess_action_for(name)).to_s
|
21
|
+
@name = name
|
22
|
+
|
23
|
+
@allow_from_states = []
|
24
|
+
@prevent_from_states = []
|
25
|
+
|
26
|
+
@before_transitions = []
|
27
|
+
@after_transitions = []
|
28
|
+
@validations = []
|
29
|
+
|
30
|
+
if block_given?
|
31
|
+
configuration = StateConfigurator.new(&block)
|
32
|
+
|
33
|
+
@allow_from_states = configuration.allow_from_states || []
|
34
|
+
@prevent_from_states = configuration.prevent_from_states || []
|
35
|
+
|
36
|
+
@after_transitions = configuration.after_transitions || []
|
37
|
+
@before_transitions = configuration.before_transitions || []
|
38
|
+
@validations = configuration.validations || []
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# @return [String] The state name as a string
|
43
|
+
def to_s
|
44
|
+
@name.to_s
|
45
|
+
end
|
46
|
+
|
47
|
+
# @return [Symbol] The state name as a string
|
48
|
+
def to_sym
|
49
|
+
@name.to_sym
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
ACTIONS = { completed: :complete, converting: :convert, invalid: :invalidate,
|
55
|
+
preparing: :prepare, processing: :process, refunded: :refund, reticulating: :reticulate,
|
56
|
+
saving: :save, searching: :search, started: :start, stopped: :stop }
|
57
|
+
|
58
|
+
def guess_action_for(name)
|
59
|
+
ACTIONS[name.to_sym]
|
60
|
+
end
|
61
|
+
|
62
|
+
class StateConfigurator
|
63
|
+
attr_reader :after_transitions, :before_transitions, :validations
|
64
|
+
attr_reader :allow_from_states, :prevent_from_states
|
65
|
+
|
66
|
+
def initialize(&block)
|
67
|
+
instance_eval(&block)
|
68
|
+
end
|
69
|
+
|
70
|
+
def allow_from(*states)
|
71
|
+
@allow_from_states ||= []
|
72
|
+
@allow_from_states |= states.map(&:to_sym)
|
73
|
+
end
|
74
|
+
|
75
|
+
def before_transition(options={})
|
76
|
+
@before_transitions ||= []
|
77
|
+
@before_transitions << options
|
78
|
+
end
|
79
|
+
|
80
|
+
def after_transition(options={})
|
81
|
+
@after_transitions ||= []
|
82
|
+
@after_transitions << options
|
83
|
+
end
|
84
|
+
|
85
|
+
def prevent_from(*states)
|
86
|
+
@prevent_from_states ||= []
|
87
|
+
@prevent_from_states |= states.map(&:to_sym)
|
88
|
+
end
|
89
|
+
|
90
|
+
def validate(options={})
|
91
|
+
@validations ||= []
|
92
|
+
@validations << options
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
data/lib/stately.rb
ADDED
@@ -0,0 +1,204 @@
|
|
1
|
+
require 'stately/machine'
|
2
|
+
require 'stately/state'
|
3
|
+
|
4
|
+
module Stately
|
5
|
+
# An InvalidTransition is an error that is raised when attempting to transition from a state
|
6
|
+
# that's not allowable, based on the Stately::State DSL definitions allow_from and prevent_from.
|
7
|
+
class InvalidTransition < StandardError
|
8
|
+
end
|
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
|
60
|
+
|
61
|
+
# Get the current Stately::Machine object
|
62
|
+
def self.stately_machine
|
63
|
+
@@stately_machine
|
64
|
+
end
|
65
|
+
|
66
|
+
# Get the current Stately::Machine object
|
67
|
+
def stately_machine
|
68
|
+
@@stately_machine
|
69
|
+
end
|
70
|
+
|
71
|
+
# Set the current Stately::Machine object
|
72
|
+
def self.stately_machine=(obj)
|
73
|
+
@@stately_machine = obj
|
74
|
+
end
|
75
|
+
|
76
|
+
# Set the current Stately::Machine object
|
77
|
+
def stately_machine=(obj)
|
78
|
+
@@stately_machine = obj
|
79
|
+
end
|
80
|
+
|
81
|
+
module InstanceMethods
|
82
|
+
# Sets up an object with Stately. The DSL is parsed and the Stately::Machine is initialized.
|
83
|
+
#
|
84
|
+
# When an object is first initialized, Stately automatically sets the state attribute to the
|
85
|
+
# start state.
|
86
|
+
#
|
87
|
+
# Additionally, a method is defined for each of the state's actions. These methods are used to
|
88
|
+
# transition between states. If you have a state named 'completed', Stately will infer the
|
89
|
+
# action to be 'complete' and define a method named 'complete'. You can then call 'complete' on
|
90
|
+
# the object to transition into the completed state.
|
91
|
+
|
92
|
+
def InstanceMethods.included(klass)
|
93
|
+
klass.class_eval do
|
94
|
+
alias_method :init_instance, :initialize
|
95
|
+
def initialize(*args)
|
96
|
+
init_instance(*args)
|
97
|
+
initialize_stately
|
98
|
+
end
|
99
|
+
|
100
|
+
stately_machine.states.each do |state|
|
101
|
+
define_method(state.action) do
|
102
|
+
transition_to(state)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# @return [Array<String>] a list of state names.
|
109
|
+
def states
|
110
|
+
stately_machine.states.map(&:name)
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
def allowed_state_transition?(to_state)
|
116
|
+
if current_state == to_state.to_s
|
117
|
+
raise InvalidTransition,
|
118
|
+
"Prevented transition from #{current_state} to #{state.to_s}."
|
119
|
+
end
|
120
|
+
|
121
|
+
allowed_from_states(to_state).include?(current_state.to_sym)
|
122
|
+
end
|
123
|
+
|
124
|
+
def allowed_from_states(state)
|
125
|
+
if state.allow_from_states.empty?
|
126
|
+
stately_machine.states.map(&:to_sym) - state.prevent_from_states
|
127
|
+
else
|
128
|
+
state.allow_from_states
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def current_state
|
133
|
+
(self.send(stately_machine.state_attr) || stately_machine.start).to_s
|
134
|
+
end
|
135
|
+
|
136
|
+
def eligible_callback?(callback)
|
137
|
+
if (callback.has_key?(:from) && callback[:from].to_s == current_state) ||
|
138
|
+
(!callback.has_key?(:from))
|
139
|
+
true
|
140
|
+
else
|
141
|
+
false
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def initialize_stately
|
146
|
+
set_initial_state
|
147
|
+
end
|
148
|
+
|
149
|
+
def run_before_transition_callbacks(state)
|
150
|
+
state.before_transitions.each do |callback|
|
151
|
+
if eligible_callback?(callback)
|
152
|
+
self.send callback[:do]
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def run_after_transition_callbacks(state)
|
158
|
+
state.after_transitions.each do |callback|
|
159
|
+
self.send callback[:do]
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def state_named(state_name)
|
164
|
+
stately_machine.states.find { |s| s.to_s == state_name.to_s }
|
165
|
+
end
|
166
|
+
|
167
|
+
def transition_to(state_name)
|
168
|
+
state = state_named(state_name)
|
169
|
+
|
170
|
+
if valid_transition_to?(state)
|
171
|
+
run_before_transition_callbacks(state)
|
172
|
+
write_attribute(stately_machine.state_attr, state.to_s)
|
173
|
+
run_after_transition_callbacks(state)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def set_initial_state
|
178
|
+
write_attribute(stately_machine.state_attr, stately_machine.start.to_s)
|
179
|
+
end
|
180
|
+
|
181
|
+
def write_attribute(attr, val)
|
182
|
+
send("#{attr}=", val)
|
183
|
+
end
|
184
|
+
|
185
|
+
def valid_transition_to?(state)
|
186
|
+
if allowed_state_transition?(state)
|
187
|
+
if state.validations.nil? || state.validations.empty?
|
188
|
+
true
|
189
|
+
else
|
190
|
+
results = state.validations.collect do |validation|
|
191
|
+
self.send validation
|
192
|
+
end
|
193
|
+
|
194
|
+
results.detect { |r| r == false }.nil?
|
195
|
+
end
|
196
|
+
else
|
197
|
+
raise InvalidTransition,
|
198
|
+
"Prevented transition from #{current_state} to #{state.to_s}."
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
require 'stately/core_ext'
|
@@ -0,0 +1,305 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe Stately do
|
5
|
+
before do
|
6
|
+
@order_class = Class.new(OpenStruct) do
|
7
|
+
stately start: :processing do
|
8
|
+
state :completed do
|
9
|
+
prevent_from :refunded
|
10
|
+
|
11
|
+
before_transition from: :processing, do: :before_completed
|
12
|
+
before_transition from: :invalid, do: :cleanup_invalid
|
13
|
+
after_transition do: :after_completed
|
14
|
+
|
15
|
+
validate :validates_amount
|
16
|
+
validate :validates_credit_card
|
17
|
+
end
|
18
|
+
|
19
|
+
state :invalid do
|
20
|
+
prevent_from :completed, :refunded
|
21
|
+
end
|
22
|
+
|
23
|
+
state :processing do
|
24
|
+
prevent_from :completed, :invalid, :refunded
|
25
|
+
end
|
26
|
+
|
27
|
+
state :refunded do
|
28
|
+
allow_from :completed
|
29
|
+
|
30
|
+
before_transition from: :completed, do: :before_refunded
|
31
|
+
after_transition from: :completed, do: :after_refunded
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def before_completed
|
38
|
+
self.serial_number = Time.now.usec
|
39
|
+
end
|
40
|
+
|
41
|
+
def after_completed
|
42
|
+
end
|
43
|
+
|
44
|
+
def before_refunded
|
45
|
+
self.refunded_reason = 'Overcharged'
|
46
|
+
end
|
47
|
+
|
48
|
+
def after_refunded
|
49
|
+
end
|
50
|
+
|
51
|
+
def cleanup_invalid
|
52
|
+
self.serial_number = nil
|
53
|
+
end
|
54
|
+
|
55
|
+
def validates_amount
|
56
|
+
amount > 0.0 && amount < 100.0
|
57
|
+
end
|
58
|
+
|
59
|
+
def validates_credit_card
|
60
|
+
self.cc_number == 123
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.should_call_callbacks_on_complete(order)
|
66
|
+
@order = order
|
67
|
+
|
68
|
+
describe 'callbacks' do
|
69
|
+
it 'calls callbacks in order' do
|
70
|
+
@order.should_receive(:before_completed).ordered
|
71
|
+
@order.should_receive(:after_completed).ordered
|
72
|
+
@order.should_not_receive :cleanup_invalid
|
73
|
+
|
74
|
+
@order.complete
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'sets serial_number' do
|
78
|
+
@order.serial_number.should be_nil
|
79
|
+
@order.complete
|
80
|
+
@order.serial_number.should_not be_nil
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.should_call_validations_on_complete(order)
|
86
|
+
@order = order
|
87
|
+
|
88
|
+
describe 'validations' do
|
89
|
+
it 'calls validations in order' do
|
90
|
+
@order.should_receive(:validates_amount).ordered
|
91
|
+
@order.should_receive(:validates_credit_card).ordered
|
92
|
+
|
93
|
+
@order.complete
|
94
|
+
end
|
95
|
+
|
96
|
+
describe 'return values' do
|
97
|
+
before do
|
98
|
+
@order.stub :validates_amount => false
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'should halt on false' do
|
102
|
+
@order.should_receive :validates_amount
|
103
|
+
@order.should_receive :validates_credit_card
|
104
|
+
@order.should_not_receive :before_completed
|
105
|
+
@order.should_not_receive :after_completed
|
106
|
+
@order.should_not_receive :cleanup_invalid
|
107
|
+
|
108
|
+
current_state = @order.state
|
109
|
+
@order.complete
|
110
|
+
@order.state.should == current_state
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def self.should_prevent_transition(from, to, action)
|
117
|
+
before do
|
118
|
+
@order = @order_class.new(amount: 99, cc_number: 123)
|
119
|
+
@order.state = from
|
120
|
+
end
|
121
|
+
|
122
|
+
it 'should be prevented' do
|
123
|
+
lambda { @order.send(action) }.should raise_error(Stately::InvalidTransition,
|
124
|
+
"Prevented transition from #{from} to #{to}.")
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def self.should_set_state(new_state, order, action)
|
129
|
+
@order = order
|
130
|
+
|
131
|
+
describe 'on success' do
|
132
|
+
before do
|
133
|
+
@order.send action
|
134
|
+
end
|
135
|
+
|
136
|
+
it 'sets state' do
|
137
|
+
@order.state.should == new_state
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
describe 'initial state' do
|
143
|
+
before do
|
144
|
+
@order = @order_class.new(amount: 99, cc_number: 123)
|
145
|
+
end
|
146
|
+
|
147
|
+
it 'creates actions for each state' do
|
148
|
+
@order_class.method_defined?(:complete).should be_true
|
149
|
+
@order_class.method_defined?(:process).should be_true
|
150
|
+
@order_class.method_defined?(:refund).should be_true
|
151
|
+
end
|
152
|
+
|
153
|
+
it 'finds all states' do
|
154
|
+
@order.states.should == [:completed, :invalid, :processing, :refunded]
|
155
|
+
end
|
156
|
+
|
157
|
+
it 'sets initial state to processing' do
|
158
|
+
@order.state.should == 'processing'
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
describe '#process' do
|
163
|
+
describe 'from processing' do
|
164
|
+
should_prevent_transition('processing', 'processing', :process)
|
165
|
+
end
|
166
|
+
|
167
|
+
describe 'from completed' do
|
168
|
+
should_prevent_transition('completed', 'processing', :process)
|
169
|
+
end
|
170
|
+
|
171
|
+
describe 'from invalid' do
|
172
|
+
should_prevent_transition('invalid', 'processing', :process)
|
173
|
+
end
|
174
|
+
|
175
|
+
describe 'from refunded' do
|
176
|
+
should_prevent_transition('refunded', 'processing', :process)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
describe '#complete' do
|
181
|
+
before do
|
182
|
+
@order = @order_class.new(amount: 99, cc_number: 123)
|
183
|
+
end
|
184
|
+
|
185
|
+
describe 'from processing' do
|
186
|
+
should_call_validations_on_complete(@order)
|
187
|
+
|
188
|
+
describe 'callbacks' do
|
189
|
+
it 'calls callbacks in order' do
|
190
|
+
@order.should_receive(:before_completed).ordered
|
191
|
+
@order.should_receive(:after_completed).ordered
|
192
|
+
@order.should_not_receive :cleanup_invalid
|
193
|
+
|
194
|
+
@order.complete
|
195
|
+
end
|
196
|
+
|
197
|
+
it 'sets serial_number' do
|
198
|
+
@order.serial_number.should be_nil
|
199
|
+
@order.complete
|
200
|
+
@order.serial_number.should_not be_nil
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
should_set_state('completed', @order, :complete)
|
205
|
+
end
|
206
|
+
|
207
|
+
describe 'from completed' do
|
208
|
+
should_prevent_transition('completed', 'completed', :complete)
|
209
|
+
end
|
210
|
+
|
211
|
+
describe 'from invalid' do
|
212
|
+
before do
|
213
|
+
@order.serial_number = Time.now.usec
|
214
|
+
@order.state = 'invalid'
|
215
|
+
end
|
216
|
+
|
217
|
+
should_call_validations_on_complete(@order)
|
218
|
+
|
219
|
+
describe 'callbacks' do
|
220
|
+
it 'calls callbacks in order' do
|
221
|
+
@order.should_receive(:cleanup_invalid).ordered
|
222
|
+
@order.should_receive(:after_completed).ordered
|
223
|
+
@order.should_not_receive :before_completed
|
224
|
+
|
225
|
+
@order.complete
|
226
|
+
end
|
227
|
+
|
228
|
+
it 'sets serial_number to nil' do
|
229
|
+
@order.serial_number.should_not be_nil
|
230
|
+
@order.complete
|
231
|
+
@order.serial_number.should be_nil
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
should_set_state('completed', @order, :complete)
|
236
|
+
end
|
237
|
+
|
238
|
+
describe 'from refunded' do
|
239
|
+
should_prevent_transition('refunded', 'completed', :complete)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
describe '#invalidate' do
|
244
|
+
describe 'from processing' do
|
245
|
+
before do
|
246
|
+
@order = @order_class.new(amount: 99, cc_number: 123)
|
247
|
+
@order.invalidate
|
248
|
+
end
|
249
|
+
|
250
|
+
it 'sets state' do
|
251
|
+
@order.state.should == 'invalid'
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
describe 'from completed' do
|
256
|
+
should_prevent_transition('completed', 'invalid', :invalidate)
|
257
|
+
end
|
258
|
+
|
259
|
+
describe 'from invalid' do
|
260
|
+
should_prevent_transition('invalid', 'invalid', :invalidate)
|
261
|
+
end
|
262
|
+
|
263
|
+
describe 'from refunded' do
|
264
|
+
should_prevent_transition('refunded', 'invalid', :invalidate)
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
describe '#refund' do
|
269
|
+
describe 'from processing' do
|
270
|
+
should_prevent_transition('processing', 'refunded', :refund)
|
271
|
+
end
|
272
|
+
|
273
|
+
describe 'from completed' do
|
274
|
+
before do
|
275
|
+
@order = @order_class.new(amount: 99, cc_number: 123)
|
276
|
+
@order.state = 'completed'
|
277
|
+
end
|
278
|
+
|
279
|
+
describe 'callbacks' do
|
280
|
+
it 'calls callbacks in order' do
|
281
|
+
@order.should_receive(:before_refunded).ordered
|
282
|
+
@order.should_receive(:after_refunded).ordered
|
283
|
+
|
284
|
+
@order.refund
|
285
|
+
end
|
286
|
+
|
287
|
+
it 'sets refunded_reason' do
|
288
|
+
@order.refunded_reason.should be_nil
|
289
|
+
@order.refund
|
290
|
+
@order.refunded_reason.should_not be_nil
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
should_set_state('refunded', @order, :refund)
|
295
|
+
end
|
296
|
+
|
297
|
+
describe 'from invalid' do
|
298
|
+
should_prevent_transition('invalid', 'refunded', :refund)
|
299
|
+
end
|
300
|
+
|
301
|
+
describe 'from refunded' do
|
302
|
+
should_prevent_transition('refunded', 'refunded', :refund)
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Stately::Machine do
|
4
|
+
before do
|
5
|
+
@machine = Stately::Machine.new(:state, :processing)
|
6
|
+
end
|
7
|
+
|
8
|
+
describe 'initialize' do
|
9
|
+
it 'sets initial vars' do
|
10
|
+
@machine.start.should == :processing
|
11
|
+
@machine.state_attr.should == :state
|
12
|
+
@machine.states.map(&:to_s).should == ['processing']
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'guesses the initial action' do
|
16
|
+
@machine.states.first.action.should == 'process'
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe '#state' do
|
21
|
+
describe 'with name only' do
|
22
|
+
describe 'of a new state' do
|
23
|
+
before do
|
24
|
+
@machine.state(:completed)
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'adds a new state' do
|
28
|
+
@machine.states.map(&:to_s).should == ['processing', 'completed']
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe 'of a previously defined state' do
|
33
|
+
before do
|
34
|
+
@machine.state(:processing)
|
35
|
+
end
|
36
|
+
|
37
|
+
it "doesn't add a new state" do
|
38
|
+
@machine.states.map(&:to_s).should == ['processing']
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe 'with name and action' do
|
44
|
+
describe 'of a new state' do
|
45
|
+
before do
|
46
|
+
@machine.state(:new_state, action: :transition_to_new_state)
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'adds a new state' do
|
50
|
+
@machine.states.map(&:to_s).should == ['processing', 'new_state']
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'adds the correct action to the new state' do
|
54
|
+
@machine.states.last.action.should == 'transition_to_new_state'
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe 'of a previously defined state' do
|
59
|
+
before do
|
60
|
+
@machine.state(:processing, action: :transition_to_processing)
|
61
|
+
end
|
62
|
+
|
63
|
+
it "doesn't add a new state" do
|
64
|
+
@machine.states.map(&:to_s).should == ['processing']
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'adds the correct action to the existing state' do
|
68
|
+
@machine.states.first.action.should == 'transition_to_processing'
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe 'with name, action, and block' do
|
74
|
+
describe 'of a new state' do
|
75
|
+
before do
|
76
|
+
@machine.state(:new_state, action: :transition_to_new_state) do
|
77
|
+
allow_from :completed
|
78
|
+
end
|
79
|
+
|
80
|
+
@new_state = @machine.states.last
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'adds a new state' do
|
84
|
+
@machine.states.map(&:to_s).should == ['processing', 'new_state']
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'adds the correct action to the new state' do
|
88
|
+
@new_state.action.should == 'transition_to_new_state'
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'includes the allow_from param' do
|
92
|
+
@new_state.allow_from_states.should == [:completed]
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
describe 'of a previously defined state' do
|
97
|
+
before do
|
98
|
+
@machine.state(:processing, action: :transition_to_processing) do
|
99
|
+
allow_from :completed
|
100
|
+
end
|
101
|
+
|
102
|
+
@new_state = @machine.states.last
|
103
|
+
end
|
104
|
+
|
105
|
+
it "doesn't add a new state" do
|
106
|
+
@machine.states.map(&:to_s).should == ['processing']
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'adds the correct action to the new state' do
|
110
|
+
@new_state.action.should == 'transition_to_processing'
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'includes the allow_from param' do
|
114
|
+
@new_state.allow_from_states.should == [:completed]
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Stately::State do
|
4
|
+
describe 'initialize' do
|
5
|
+
describe 'with a block given' do
|
6
|
+
describe 'new' do
|
7
|
+
before do
|
8
|
+
@state = Stately::State.new(:invalid, nil) do
|
9
|
+
allow_from :completed
|
10
|
+
prevent_from :completed, :refunded
|
11
|
+
|
12
|
+
before_transition do: :prepare
|
13
|
+
before_transition from: :processing, do: :before_completed
|
14
|
+
after_transition do: :cleanup
|
15
|
+
after_transition from: :processing, do: :after_processing
|
16
|
+
|
17
|
+
validate :validates_amount
|
18
|
+
validate :validates_credit_card
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'should set initial values' do
|
23
|
+
@state.name.should == :invalid
|
24
|
+
|
25
|
+
@state.allow_from_states.should == [:completed]
|
26
|
+
@state.prevent_from_states.should == [:completed, :refunded]
|
27
|
+
|
28
|
+
@state.before_transitions.should == [{do: :prepare}, {from: :processing,
|
29
|
+
do: :before_completed}]
|
30
|
+
@state.after_transitions.should == [{do: :cleanup}, {from: :processing,
|
31
|
+
do: :after_processing}]
|
32
|
+
@state.validations.should == [:validates_amount, :validates_credit_card]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe 'without a block given' do
|
38
|
+
describe 'new' do
|
39
|
+
before do
|
40
|
+
@state = Stately::State.new(:test_state)
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'should set initial values' do
|
44
|
+
@state.name.should == :test_state
|
45
|
+
|
46
|
+
@state.allow_from_states.should == []
|
47
|
+
@state.prevent_from_states.should == []
|
48
|
+
|
49
|
+
@state.before_transitions.should == []
|
50
|
+
@state.after_transitions.should == []
|
51
|
+
@state.validations.should == []
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe 'with a given action' do
|
56
|
+
before do
|
57
|
+
@state = Stately::State.new(:test_state, :test_action)
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'should set the given action name' do
|
61
|
+
@state.action.should == 'test_action'
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe 'without a given action' do
|
66
|
+
before do
|
67
|
+
@actions = { completed: :complete, converting: :convert, invalid: :invalidate,
|
68
|
+
preparing: :prepare, processing: :process, refunded: :refund, reticulating: :reticulate,
|
69
|
+
saving: :save, searching: :search, started: :start, stopped: :stop }
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'should set the correct action verb' do
|
73
|
+
@actions.map do |state_name, action_name|
|
74
|
+
state = Stately::State.new(state_name)
|
75
|
+
state.action.should == action_name.to_s
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
describe '#to_s' do
|
83
|
+
before do
|
84
|
+
@state = Stately::State.new(:test_state)
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'should return a string' do
|
88
|
+
@state.to_s.should == 'test_state'
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
describe '#to_sym' do
|
93
|
+
before do
|
94
|
+
@state = Stately::State.new('test_state')
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'should return a symbol' do
|
98
|
+
@state.to_sym.should == :test_state
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe Stately::InstanceMethods do
|
5
|
+
before do
|
6
|
+
@test_class = Class.new(Object) do
|
7
|
+
attr_accessor :state
|
8
|
+
|
9
|
+
stately start: :processing do
|
10
|
+
state :completed
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
@object = @test_class.new
|
15
|
+
end
|
16
|
+
|
17
|
+
describe 'initialize' do
|
18
|
+
it 'creates a new Stately::Machine' do
|
19
|
+
@object.stately_machine.class.should == Stately::Machine
|
20
|
+
@object.stately_machine.should == @test_class.stately_machine
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'sets initial state' do
|
24
|
+
@object.state.should == 'processing'
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe '#states' do
|
29
|
+
it 'returns known state names in order' do
|
30
|
+
@object.states.should == [:processing, :completed]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe 'actions' do
|
35
|
+
it 'defines action methods' do
|
36
|
+
@test_class.method_defined?(:complete).should be_true
|
37
|
+
@test_class.method_defined?(:process).should be_true
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe 'stately_machine' do
|
42
|
+
it 'defines a class-level accessor called stately_machine' do
|
43
|
+
@test_class.respond_to?(:stately_machine).should be_true
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'defines an instance-level accessor called stately_machine' do
|
47
|
+
@test_class.method_defined?(:stately_machine).should be_true
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'defines a class-level setter called stately_machine=' do
|
51
|
+
@test_class.respond_to?(:stately_machine=).should be_true
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'defines an instance-level setter called stately_machine=' do
|
55
|
+
@test_class.method_defined?(:stately_machine=).should be_true
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
data/stately.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
$LOAD_PATH << File.expand_path('../lib', __FILE__)
|
2
|
+
require 'stately/version'
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = 'stately'
|
6
|
+
s.version = Stately::VERSION
|
7
|
+
s.authors = ['Ryan Twomey']
|
8
|
+
s.email = ['rtwomey@gmail.com']
|
9
|
+
s.homepage = 'http://github.com/rtwomey/stately'
|
10
|
+
s.summary = 'A simple, elegant state machine for Ruby'
|
11
|
+
s.description = 'Add an elegant state machine to your ruby objects with a simple DSL'
|
12
|
+
|
13
|
+
s.files = `git ls-files`.split("\n")
|
14
|
+
s.test_files = `git ls-files -- {spec}/*`.split("\n")
|
15
|
+
|
16
|
+
s.add_development_dependency 'redcarpet', '~> 2.2.2'
|
17
|
+
s.add_development_dependency 'rspec', '~> 2.0'
|
18
|
+
s.add_development_dependency 'yard', '~> 0.8.3'
|
19
|
+
|
20
|
+
s.required_ruby_version = Gem::Requirement.new('>= 1.9.2')
|
21
|
+
s.require_paths = ['lib']
|
22
|
+
end
|
metadata
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: stately
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Ryan Twomey
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-11-03 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: redcarpet
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 2.2.2
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 2.2.2
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rspec
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '2.0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '2.0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: yard
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 0.8.3
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.8.3
|
62
|
+
description: Add an elegant state machine to your ruby objects with a simple DSL
|
63
|
+
email:
|
64
|
+
- rtwomey@gmail.com
|
65
|
+
executables: []
|
66
|
+
extensions: []
|
67
|
+
extra_rdoc_files: []
|
68
|
+
files:
|
69
|
+
- .gitignore
|
70
|
+
- .rspec
|
71
|
+
- .yardopts
|
72
|
+
- Gemfile
|
73
|
+
- Gemfile.lock
|
74
|
+
- MIT-LICENSE
|
75
|
+
- README.md
|
76
|
+
- Rakefile
|
77
|
+
- lib/stately.rb
|
78
|
+
- lib/stately/core_ext.rb
|
79
|
+
- lib/stately/machine.rb
|
80
|
+
- lib/stately/state.rb
|
81
|
+
- lib/stately/version.rb
|
82
|
+
- lib/tasks/stately_tasks.rake
|
83
|
+
- spec/functional/stately_spec.rb
|
84
|
+
- spec/spec_helper.rb
|
85
|
+
- spec/unit/stately/machine_spec.rb
|
86
|
+
- spec/unit/stately/state_spec.rb
|
87
|
+
- spec/unit/stately_spec.rb
|
88
|
+
- stately.gemspec
|
89
|
+
homepage: http://github.com/rtwomey/stately
|
90
|
+
licenses: []
|
91
|
+
post_install_message:
|
92
|
+
rdoc_options: []
|
93
|
+
require_paths:
|
94
|
+
- lib
|
95
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
96
|
+
none: false
|
97
|
+
requirements:
|
98
|
+
- - ! '>='
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: 1.9.2
|
101
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
102
|
+
none: false
|
103
|
+
requirements:
|
104
|
+
- - ! '>='
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: '0'
|
107
|
+
requirements: []
|
108
|
+
rubyforge_project:
|
109
|
+
rubygems_version: 1.8.24
|
110
|
+
signing_key:
|
111
|
+
specification_version: 3
|
112
|
+
summary: A simple, elegant state machine for Ruby
|
113
|
+
test_files: []
|
114
|
+
has_rdoc:
|