end_state 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: fd9f6c8fbf5f8ec9fd075dabbd2011aa51420465
4
+ data.tar.gz: 12ba88eafad32a520b119756773f723482054536
5
+ SHA512:
6
+ metadata.gz: 569f287932d9c85bb81ff72bfba03e8c5d78d1ea03d492a2ba8379213f794e5fe12f445ee13eab17036c775789a9a88c4525256b71738ca07601c48850c6ea9d
7
+ data.tar.gz: 497920437e5eea536f63249ffe9bf84875793ab2e39c276cbf1d56e627953a90a05967500acb503f03802b3ad115d5bc4d680f7e528e51052a89c9eb6cd21681
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.hound.yml ADDED
@@ -0,0 +1,17 @@
1
+ AllCops:
2
+ Include:
3
+ - Rakefile
4
+ - config.ru
5
+ Exclude:
6
+ - db/**
7
+ - config/**
8
+ - spec/*helper.rb
9
+
10
+ LineLength:
11
+ Enabled: false
12
+
13
+ Documentation:
14
+ Enabled: false
15
+
16
+ AndOr:
17
+ Enabled: false
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.rubocop.yml ADDED
@@ -0,0 +1,2 @@
1
+ inherit_from:
2
+ ./.hound.yml
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.1
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in end_state.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 alexpeachey
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,202 @@
1
+ # EndState
2
+
3
+ EndState is an unobtrusive way to add state machines to your application.
4
+
5
+ An `EndState::StateMachine` acts as a decorator of sorts for your stateful object.
6
+ Your stateful object does not need to know it is being used in a state machine and
7
+ only needs to respond to `state` and `state=`. (This is customizable)
8
+
9
+ The control flow for guarding against transitions and performing post-transition
10
+ operations is handled by classes you create allowing maximum separation of responsibilities.
11
+
12
+ ## Installation
13
+
14
+ Add this line to your application's Gemfile:
15
+
16
+ gem 'end_state'
17
+
18
+ And then execute:
19
+
20
+ $ bundle
21
+
22
+ Or install it yourself as:
23
+
24
+ $ gem install end_state
25
+
26
+ ## StateMachine
27
+
28
+ Create a state machine by subclassing `EndState::StateMachine`.
29
+
30
+ ```ruby
31
+ class Machine < EndState::StateMachine
32
+ transition a: :b
33
+ transition b: :c
34
+ transition [:b, :c] => :a
35
+ end
36
+ ```
37
+
38
+ Use it by wrapping a stateful object.
39
+
40
+ ```ruby
41
+ class StatefulObject
42
+ attr_accessor :state
43
+
44
+ def initialize(state)
45
+ @state = state
46
+ end
47
+ end
48
+
49
+ machine = Machine.new(StatefulObject.new(:a))
50
+
51
+ machine.transition :b # => true
52
+ machine.state # => :b
53
+ machine.b? # => true
54
+ machine.c! # => true
55
+ machine.state # => :c
56
+ machine.can_transition? :b # => false
57
+ machine.can_transition? :a # => true
58
+ machine.b! # => false
59
+ machine.a! # => true
60
+ machine.state # => :a
61
+ ```
62
+
63
+ ## Guards
64
+
65
+ Guards can be created by subclassing `EndState::Guard`. Your class will be provided access to:
66
+
67
+ * `object` - The wrapped object.
68
+ * `state` - The desired state.
69
+ * `params` - A hash of params as set in the transition definition.
70
+
71
+ Your class should implement the `will_allow?` method which must return true or false.
72
+
73
+ Optionally you can implement the `passed` and/or `failed` methods which will be called after the guard passes or fails.
74
+ These will only be called during the check performed during the transition and will not be fired when asking `can_transition?`.
75
+ These hooks can be useful for things like logging.
76
+
77
+ The wrapped object has an array `failure_messages` available for tracking reasons for invalid transitions. You may shovel
78
+ a reason (string) into this if you want to provide information on why your guard failed.
79
+
80
+ ```ruby
81
+ class EasyGuard < EndState::Guard
82
+ def will_allow?
83
+ true
84
+ end
85
+
86
+ def failed
87
+ Rails.logger.error "Failed to transition to state #{state} from #{object.state}."
88
+ end
89
+ end
90
+ ```
91
+
92
+ A guard can be added to the transition definition:
93
+
94
+ ```ruby
95
+ class Machine < EndState::StateMachine
96
+ transition a: :b do |t|
97
+ t.guard EasyGuard
98
+ t.guard SomeOtherGuard, option1: 'Some Option', option2: 'Some Other Option'
99
+ end
100
+ end
101
+ ```
102
+
103
+ ## Finalizers
104
+
105
+ Finalizers can be created by subclassing `EndState::Finalizer`. Your class will be provided access to:
106
+
107
+ * `object` - The wrapped object that has been transitioned.
108
+ * `state` - The previous state.
109
+ * `params` - A hash of params as set in the transition definition.
110
+
111
+ Your class should implement the `call` method which should return true or false as to whether it was successful or not.
112
+
113
+ If your finalizer returns false, the transition will be "rolled back" and the failing transition, as well as all previous transitions
114
+ will be rolled back. The roll back is performed by calling `rollback` on the finalizer. During the roll back the finalizer will be
115
+ set up a little differently and you have access to:
116
+
117
+ * `object` - The wrapped object that has been rolled back.
118
+ * `state` - The attempted desired state.
119
+ * `params` - A hash of params as set in the transition definition.
120
+
121
+ The wrapped object has an array `failure_messages` available for tracking reasons for invalid transitions. You may shovel
122
+ a reason (string) into this if you want to provide information on why your finalizer failed.
123
+
124
+ ```ruby
125
+ class WrapUp < EndState::Finalizer
126
+ def call
127
+ # Some important processing
128
+ true
129
+ end
130
+
131
+ def rollback
132
+ # Undo stuff that shouldn't have been done.
133
+ end
134
+ end
135
+ ```
136
+
137
+ A finalizer can be added to the transition definition:
138
+
139
+ ```ruby
140
+ class Machine < EndState::StateMachine
141
+ transition a: :b do |t|
142
+ t.finalizer WrapUp, option1: 'Some Option', option2: 'Some Other Option'
143
+ end
144
+ end
145
+ ```
146
+
147
+ Since it is a common use case, a finalizer is included which will call `save` on the wrapped object if it responds to `save`.
148
+ You can use this with a convience method in your transition definition:
149
+
150
+ ```ruby
151
+ class Machine < EndState::StateMachine
152
+ transition a: :b do |t|
153
+ t.persistence_on
154
+ end
155
+ end
156
+ ```
157
+
158
+ ## Action
159
+
160
+ By default, a transition from one state to another is handled by `EndState` and only changes the state to the new state.
161
+ This is the recommended default and you should have a good reason to do something more or different.
162
+ If you really want to do something different though you can create a class that subclasses `EndState::Action` and implement
163
+ the `call` method.
164
+
165
+ You will have access to:
166
+
167
+ * `object` - The wrapped object.
168
+ * `state` - The desired state.
169
+
170
+ ```ruby
171
+ class MyCustomAction < EndState::Action
172
+ def call
173
+ # Do something special
174
+ super
175
+ end
176
+ end
177
+ ```
178
+
179
+ ```ruby
180
+ class Machine < EndState::StateMachine
181
+ transition a: :b do |t|
182
+ t.custom_action MyCustomAction
183
+ end
184
+ end
185
+ ```
186
+
187
+ ## Exceptions for failing Transitions
188
+
189
+ By default `transition` will only raise an exception, `EndState::UnknownState`, if called with a state that doesn't exist.
190
+ All other failures, such as missing transition, guard failure, or finalizer failure will silently just return `false` and not
191
+ transition to the new state.
192
+
193
+ You also have the option to use `transition!` which will instead raise an error for failures. If your guards and/or finalizers
194
+ add to the `failure_messages` array then they will be included in the error message.
195
+
196
+ ## Contributing
197
+
198
+ 1. Fork it ( https://github.com/Originate/end_state/fork )
199
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
200
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
201
+ 4. Push to the branch (`git push origin my-new-feature`)
202
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/end_state.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'end_state/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'end_state'
8
+ spec.version = EndState::VERSION
9
+ spec.authors = ['alexpeachey']
10
+ spec.email = ['alex.peachey@gmail.com']
11
+ spec.summary = 'A State Machine implementation'
12
+ spec.description = 'A modular state machine with single responsibilities.'
13
+ spec.homepage = 'https://github.com/Originate/end_state'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(/^bin\//) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(/^(test|spec|features)\//)
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_development_dependency 'bundler', '~> 1.5'
22
+ spec.add_development_dependency 'rake'
23
+ spec.add_development_dependency 'rspec'
24
+ spec.add_development_dependency 'rubocop'
25
+ end
@@ -0,0 +1,70 @@
1
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+ require 'end_state'
3
+
4
+ class Easy < EndState::Guard
5
+ def will_allow?
6
+ true
7
+ end
8
+ end
9
+
10
+ class NoOp < EndState::Finalizer
11
+ def call
12
+ true
13
+ end
14
+
15
+ def rollback
16
+ true
17
+ end
18
+ end
19
+
20
+ class CustomAction < EndState::Action
21
+ def call
22
+ super
23
+ end
24
+ end
25
+
26
+ class Machine < EndState::StateMachine
27
+ transition a: :b do |t|
28
+ t.guard Easy, important_param: 'FOO!'
29
+ t.persistence_on
30
+ end
31
+
32
+ transition b: :c do |t|
33
+ t.custom_action CustomAction
34
+ t.persistence_on
35
+ end
36
+
37
+ transition [:b, :c] => :a do |t|
38
+ t.finalizer NoOp, not_very_important_param: 'Ignore me'
39
+ t.persistence_on
40
+ end
41
+ end
42
+
43
+ class StatefulObject
44
+ attr_accessor :state
45
+
46
+ def initialize(state)
47
+ @state = state
48
+ end
49
+
50
+ def save
51
+ puts "Saved with state: #{state}"
52
+ true
53
+ end
54
+ end
55
+
56
+ object = StatefulObject.new(:a)
57
+ machine = Machine.new(object)
58
+
59
+ puts "The machine's class is: #{machine.class.name}"
60
+ puts "The machine's object class is: #{machine.object.class.name}"
61
+ puts
62
+
63
+ %i( b c a c).each do |state|
64
+ puts "Attempting to move to #{state}"
65
+ machine.transition state
66
+ puts "State: #{machine.state}"
67
+ predicate = "#{state}?".to_sym
68
+ puts "#{state}?: #{machine.send(predicate)}"
69
+ puts
70
+ end
@@ -0,0 +1,19 @@
1
+ module EndState
2
+ class Action
3
+ attr_reader :object, :state
4
+
5
+ def initialize(object, state)
6
+ @object = object
7
+ @state = state
8
+ end
9
+
10
+ def call
11
+ object.state = state
12
+ true
13
+ end
14
+
15
+ def rollback
16
+ call
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,7 @@
1
+ module EndState
2
+ class Error < StandardError; end
3
+ class UnknownState < Error; end
4
+ class UnknownTransition < Error; end
5
+ class GuardFailed < Error; end
6
+ class FinalizerFailed < Error; end
7
+ end
@@ -0,0 +1,19 @@
1
+ module EndState
2
+ class Finalizer
3
+ attr_reader :object, :state, :params
4
+
5
+ def initialize(object, state, params)
6
+ @object = object
7
+ @state = state
8
+ @params = params
9
+ end
10
+
11
+ def call
12
+ false
13
+ end
14
+
15
+ def rollback
16
+ true
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,15 @@
1
+ module EndState
2
+ module Finalizers
3
+ class Persistence < EndState::Finalizer
4
+ def call
5
+ return false unless object.respond_to? :save
6
+ !!(object.save)
7
+ end
8
+
9
+ def rollback
10
+ return true unless object.respond_to? :save
11
+ !!(object.save)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1 @@
1
+ require 'end_state/finalizers/persistence'
@@ -0,0 +1,28 @@
1
+ module EndState
2
+ class Guard
3
+ attr_reader :object, :state, :params
4
+
5
+ def initialize(object, state, params)
6
+ @object = object
7
+ @state = state
8
+ @params = params
9
+ end
10
+
11
+ def allowed?
12
+ will_allow?.tap do |result|
13
+ failed unless result
14
+ passed if result
15
+ end
16
+ end
17
+
18
+ def will_allow?
19
+ false
20
+ end
21
+
22
+ def passed
23
+ end
24
+
25
+ def failed
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,93 @@
1
+ module EndState
2
+ class StateMachine < SimpleDelegator
3
+ attr_accessor :failure_messages
4
+
5
+ def self.transition(state_map, &block)
6
+ final_state = state_map.values.first
7
+ transition = Transition.new(final_state)
8
+ Array(state_map.keys.first).each do |state|
9
+ transitions[{ state => final_state }] = transition
10
+ end
11
+ yield transition if block
12
+ end
13
+
14
+ def self.transitions
15
+ @transitions ||= {}
16
+ end
17
+
18
+ def self.state_attribute(attribute)
19
+ define_method(:state) { send(attribute.to_sym) }
20
+ define_method(:state=) { |val| send("#{attribute}=".to_sym, val) }
21
+ end
22
+
23
+ def self.states
24
+ (start_states + end_states).uniq
25
+ end
26
+
27
+ def self.start_states
28
+ transitions.keys.map { |state_map| state_map.keys.first }.uniq
29
+ end
30
+
31
+ def self.end_states
32
+ transitions.keys.map { |state_map| state_map.values.first }.uniq
33
+ end
34
+
35
+ def object
36
+ __getobj__
37
+ end
38
+
39
+ def can_transition?(state)
40
+ previous_state = self.state
41
+ transition = self.class.transitions[{ previous_state => state }]
42
+ return block_transistion(transition, state, :soft) unless transition
43
+ transition.will_allow? state
44
+ end
45
+
46
+ def transition(state, mode = :soft)
47
+ @failure_messages = []
48
+ previous_state = self.state
49
+ transition = self.class.transitions[{ previous_state => state }]
50
+ return block_transistion(transition, state, mode) unless transition
51
+ return guard_failed(state, mode) unless transition.allowed?(self)
52
+ return false unless transition.action.new(self, state).call
53
+ return finalize_failed(state, mode) unless transition.finalize(self, previous_state)
54
+ true
55
+ end
56
+
57
+ def transition!(state)
58
+ transition state, :hard
59
+ end
60
+
61
+ def method_missing(method, *args, &block)
62
+ check_state = method.to_s[0..-2].to_sym
63
+ return super unless self.class.states.include? check_state
64
+ if method.to_s.end_with?('?')
65
+ state == check_state
66
+ elsif method.to_s.end_with?('!')
67
+ transition check_state
68
+ else
69
+ super
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ def block_transistion(transition, state, mode)
76
+ if self.class.end_states.include? state
77
+ fail UnknownTransition, "The transition: #{object.state} => #{state} is unknown." if mode == :hard
78
+ return false
79
+ end
80
+ fail UnknownState, "The state: #{state} is unknown." unless transition
81
+ end
82
+
83
+ def guard_failed(state, mode)
84
+ return false unless mode == :hard
85
+ fail GuardFailed, "The transition to #{state} was blocked: #{failure_messages.join(', ')}"
86
+ end
87
+
88
+ def finalize_failed(state, mode)
89
+ return false unless mode == :hard
90
+ fail FinalizerFailed, "The transition to #{state} was rolled back: #{failure_messages.join(', ')}"
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,57 @@
1
+ module EndState
2
+ class Transition
3
+ attr_reader :state
4
+ attr_accessor :action, :guards, :finalizers
5
+
6
+ def initialize(state)
7
+ @state = state
8
+ @action = Action
9
+ @guards = []
10
+ @finalizers = []
11
+ end
12
+
13
+ def allowed?(object)
14
+ guards.all? { |guard| guard[:guard].new(object, state, guard[:params]).allowed? }
15
+ end
16
+
17
+ def will_allow?(object)
18
+ guards.all? { |guard| guard[:guard].new(object, state, guard[:params]).will_allow? }
19
+ end
20
+
21
+ def finalize(object, previous_state)
22
+ finalizers.each_with_object([]) do |finalizer, finalized|
23
+ finalized << finalizer
24
+ return rollback(finalized, object, previous_state) unless run_finalizer(finalizer, object, state)
25
+ end
26
+ true
27
+ end
28
+
29
+ def custom_action(action)
30
+ @action = action
31
+ end
32
+
33
+ def guard(guard, params = {})
34
+ guards << { guard: guard, params: params }
35
+ end
36
+
37
+ def finalizer(finalizer, params = {})
38
+ finalizers << { finalizer: finalizer, params: params }
39
+ end
40
+
41
+ def persistence_on
42
+ finalizer Finalizers::Persistence
43
+ end
44
+
45
+ private
46
+
47
+ def rollback(finalized, object, previous_state)
48
+ action.new(object, previous_state).rollback
49
+ finalized.reverse.each { |f| f[:finalizer].new(object, state, f[:params]).rollback }
50
+ false
51
+ end
52
+
53
+ def run_finalizer(finalizer, object, state)
54
+ finalizer[:finalizer].new(object, state, finalizer[:params]).call
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,3 @@
1
+ module EndState
2
+ VERSION = "0.0.1"
3
+ end
data/lib/end_state.rb ADDED
@@ -0,0 +1,12 @@
1
+ require 'delegate'
2
+ require 'end_state/version'
3
+ require 'end_state/errors'
4
+ require 'end_state/guard'
5
+ require 'end_state/finalizer'
6
+ require 'end_state/finalizers'
7
+ require 'end_state/transition'
8
+ require 'end_state/action'
9
+ require 'end_state/state_machine'
10
+
11
+ module EndState
12
+ end
@@ -0,0 +1,23 @@
1
+ require 'spec_helper'
2
+
3
+ module EndState
4
+ describe Action do
5
+ subject(:action) { Action.new(object, state) }
6
+ let(:object) { OpenStruct.new(state: nil) }
7
+ let(:state) { :a }
8
+
9
+ describe '#call' do
10
+ it 'changes the state to the new state' do
11
+ action.call
12
+ expect(object.state).to eq :a
13
+ end
14
+ end
15
+
16
+ describe '#rollback' do
17
+ it 'changes the state to the new state' do
18
+ action.rollback
19
+ expect(object.state).to eq :a
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,42 @@
1
+ require 'spec_helper'
2
+
3
+ module EndState
4
+ module Finalizers
5
+ describe Persistence do
6
+ subject(:finalizer) { Persistence.new(object, state, params) }
7
+ let(:object) { double :object, save: nil }
8
+ let(:state) { :b }
9
+ let(:params) { {} }
10
+
11
+ describe '#call' do
12
+ it 'calls save on the object' do
13
+ finalizer.call
14
+ expect(object).to have_received(:save)
15
+ end
16
+
17
+ context 'when the object does not respond to save' do
18
+ let(:object) { Object.new }
19
+
20
+ it 'returns false' do
21
+ expect(finalizer.call).to be_false
22
+ end
23
+ end
24
+ end
25
+
26
+ describe '#rollback' do
27
+ it 'calls save on the object' do
28
+ finalizer.rollback
29
+ expect(object).to have_received(:save)
30
+ end
31
+
32
+ context 'when the object does not respond to save' do
33
+ let(:object) { Object.new }
34
+
35
+ it 'returns true' do
36
+ expect(finalizer.rollback).to be_true
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,327 @@
1
+ require 'spec_helper'
2
+ require 'ostruct'
3
+
4
+ module EndState
5
+ describe StateMachine do
6
+ subject(:machine) { StateMachine.new(object) }
7
+ let(:object) { OpenStruct.new(state: nil) }
8
+ before { StateMachine.instance_variable_set '@transitions'.to_sym, nil }
9
+
10
+ describe '.transition' do
11
+ let(:state_map) { { a: :b } }
12
+ let(:yielded) { OpenStruct.new(transition: nil) }
13
+ before { StateMachine.transition(state_map) { |transition| yielded.transition = transition } }
14
+
15
+ it 'yields a transition for the supplied end state' do
16
+ expect(yielded.transition.state).to eq :b
17
+ end
18
+
19
+ it 'does not require a block' do
20
+ expect(StateMachine.transition(b: :c)).not_to raise_error
21
+ end
22
+
23
+ it 'adds the transition to the state machine' do
24
+ expect(StateMachine.transitions[state_map]).to eq yielded.transition
25
+ end
26
+ end
27
+
28
+ describe '.state_attribute' do
29
+ context 'when set to :foobar' do
30
+ let(:object) { OpenStruct.new(foobar: :a) }
31
+ before { StateMachine.state_attribute :foobar }
32
+
33
+ it 'answers state with foobar' do
34
+ expect(machine.state).to eq object.foobar
35
+ end
36
+
37
+ it 'answers state= with foobar=' do
38
+ machine.state = :b
39
+ expect(object.foobar).to eq :b
40
+ end
41
+
42
+ after do
43
+ StateMachine.send(:remove_method, :state)
44
+ StateMachine.send(:remove_method, :state=)
45
+ end
46
+ end
47
+ end
48
+
49
+ describe '.states' do
50
+ before do
51
+ StateMachine.transition(a: :b)
52
+ StateMachine.transition(b: :c)
53
+ end
54
+
55
+ specify { expect(StateMachine.states).to eq [:a, :b, :c] }
56
+ end
57
+
58
+ describe '.start_states' do
59
+ before do
60
+ StateMachine.transition(a: :b)
61
+ StateMachine.transition(b: :c)
62
+ end
63
+
64
+ specify { expect(StateMachine.start_states).to eq [:a, :b] }
65
+ end
66
+
67
+ describe '.end_states' do
68
+ before do
69
+ StateMachine.transition(a: :b)
70
+ StateMachine.transition(b: :c)
71
+ end
72
+
73
+ specify { expect(StateMachine.end_states).to eq [:b, :c] }
74
+ end
75
+
76
+ describe '#state' do
77
+ context 'when the object has state :a' do
78
+ let(:object) { OpenStruct.new(state: :a) }
79
+
80
+ specify { expect(machine.state).to eq :a }
81
+ end
82
+
83
+ context 'when the object has state :b' do
84
+ let(:object) { OpenStruct.new(state: :b) }
85
+
86
+ specify { expect(machine.state).to eq :b }
87
+ end
88
+ end
89
+
90
+ describe '#{state}?' do
91
+ before do
92
+ StateMachine.transition a: :b
93
+ StateMachine.transition b: :c
94
+ end
95
+
96
+ context 'when the object has state :a' do
97
+ let(:object) { OpenStruct.new(state: :a) }
98
+
99
+ specify { expect(machine.a?).to be_true }
100
+ specify { expect(machine.b?).to be_false }
101
+ end
102
+
103
+ context 'when the object has state :b' do
104
+ let(:object) { OpenStruct.new(state: :b) }
105
+
106
+ specify { expect(machine.b?).to be_true }
107
+ specify { expect(machine.a?).to be_false }
108
+ end
109
+ end
110
+
111
+ describe '#{state}!' do
112
+ let(:object) { OpenStruct.new(state: :a) }
113
+ before do
114
+ StateMachine.transition a: :b
115
+ end
116
+
117
+ it 'transitions the state' do
118
+ machine.b!
119
+ expect(machine.state).to eq :b
120
+ end
121
+ end
122
+
123
+ describe '#can_transition?' do
124
+ let(:object) { OpenStruct.new(state: :a) }
125
+ before do
126
+ StateMachine.transition a: :b
127
+ StateMachine.transition b: :c
128
+ end
129
+
130
+ context 'when asking about an allowed transition' do
131
+ specify { expect(machine.can_transition? :b).to be_true }
132
+ end
133
+
134
+ context 'when asking about a disallowed transition' do
135
+ specify { expect(machine.can_transition? :c).to be_false }
136
+ end
137
+ end
138
+
139
+ describe '#transition' do
140
+ context 'when the transition does not exist' do
141
+ it 'raises an unknown state error' do
142
+ expect { machine.transition(:no_state) }.to raise_error(UnknownState)
143
+ end
144
+
145
+ context 'but the attempted state does exist' do
146
+ before { StateMachine.transition a: :b }
147
+
148
+ it 'returns false' do
149
+ expect(machine.transition(:b)).to be_false
150
+ end
151
+ end
152
+ end
153
+
154
+ context 'when the transition does exist' do
155
+ before { object.state = :a }
156
+
157
+ context 'and no configuration is given' do
158
+ before { StateMachine.transition a: :b }
159
+
160
+ it 'transitions the state' do
161
+ machine.transition :b
162
+ expect(object.state).to eq :b
163
+ end
164
+ end
165
+
166
+ context 'and a guard is configured' do
167
+ let(:guard) { double :guard, new: guard_instance }
168
+ let(:guard_instance) { double :guard_instance, allowed?: nil }
169
+ before do
170
+ StateMachine.transition a: :b
171
+ StateMachine.transitions[{ a: :b }].guards << { guard: guard, params: {} }
172
+ end
173
+
174
+ context 'and the object satisfies the guard' do
175
+ before do
176
+ guard_instance.stub(:allowed?).and_return(true)
177
+ object.state = :a
178
+ end
179
+
180
+ it 'transitions the state' do
181
+ machine.transition :b
182
+ expect(object.state).to eq :b
183
+ end
184
+ end
185
+
186
+ context 'and the object does not satisfy the guard' do
187
+ before do
188
+ guard_instance.stub(:allowed?).and_return(false)
189
+ object.state = :a
190
+ end
191
+
192
+ it 'does not transition the state' do
193
+ machine.transition :b
194
+ expect(object.state).to eq :a
195
+ end
196
+ end
197
+ end
198
+
199
+ context 'and a finalizer is configured' do
200
+ before do
201
+ StateMachine.transition a: :b do |transition|
202
+ transition.persistence_on
203
+ end
204
+ end
205
+
206
+ context 'and the finalizer is successful' do
207
+ before do
208
+ object.state = :a
209
+ object.stub(:save).and_return(true)
210
+ end
211
+
212
+ it 'transitions the state' do
213
+ machine.transition :b
214
+ expect(object.state).to eq :b
215
+ end
216
+ end
217
+
218
+ context 'and the finalizer fails' do
219
+ before do
220
+ object.state = :a
221
+ object.stub(:save).and_return(false)
222
+ end
223
+
224
+ it 'does not transition the state' do
225
+ machine.transition :b
226
+ expect(object.state).to eq :a
227
+ end
228
+ end
229
+ end
230
+ end
231
+ end
232
+
233
+ describe '#transition!' do
234
+ context 'when the transition does not exist' do
235
+ it 'raises an unknown state error' do
236
+ expect { machine.transition!(:no_state) }.to raise_error(UnknownState)
237
+ end
238
+
239
+ context 'but the attempted state does exist' do
240
+ before { StateMachine.transition a: :b }
241
+
242
+ it 'returns false' do
243
+ expect { machine.transition!(:b) }.to raise_error(UnknownTransition)
244
+ end
245
+ end
246
+ end
247
+
248
+ context 'when the transition does exist' do
249
+ before { object.state = :a }
250
+
251
+ context 'and no configuration is given' do
252
+ before { StateMachine.transition a: :b }
253
+
254
+ it 'transitions the state' do
255
+ machine.transition! :b
256
+ expect(object.state).to eq :b
257
+ end
258
+ end
259
+
260
+ context 'and a guard is configured' do
261
+ let(:guard) { double :guard, new: guard_instance }
262
+ let(:guard_instance) { double :guard_instance, allowed?: nil }
263
+ before do
264
+ StateMachine.transition a: :b
265
+ StateMachine.transitions[{ a: :b }].guards << { guard: guard, params: {} }
266
+ end
267
+
268
+ context 'and the object satisfies the guard' do
269
+ before do
270
+ guard_instance.stub(:allowed?).and_return(true)
271
+ object.state = :a
272
+ end
273
+
274
+ it 'transitions the state' do
275
+ machine.transition! :b
276
+ expect(object.state).to eq :b
277
+ end
278
+ end
279
+
280
+ context 'and the object does not satisfy the guard' do
281
+ before do
282
+ guard_instance.stub(:allowed?).and_return(false)
283
+ object.state = :a
284
+ end
285
+
286
+ it 'does not transition the state' do
287
+ expect { machine.transition! :b }.to raise_error(GuardFailed)
288
+ expect(object.state).to eq :a
289
+ end
290
+ end
291
+ end
292
+
293
+ context 'and a finalizer is configured' do
294
+ before do
295
+ StateMachine.transition a: :b do |transition|
296
+ transition.persistence_on
297
+ end
298
+ end
299
+
300
+ context 'and the finalizer is successful' do
301
+ before do
302
+ object.state = :a
303
+ object.stub(:save).and_return(true)
304
+ end
305
+
306
+ it 'transitions the state' do
307
+ machine.transition! :b
308
+ expect(object.state).to eq :b
309
+ end
310
+ end
311
+
312
+ context 'and the finalizer fails' do
313
+ before do
314
+ object.state = :a
315
+ object.stub(:save).and_return(false)
316
+ end
317
+
318
+ it 'does not transition the state' do
319
+ expect { machine.transition! :b }.to raise_error(FinalizerFailed)
320
+ expect(object.state).to eq :a
321
+ end
322
+ end
323
+ end
324
+ end
325
+ end
326
+ end
327
+ end
@@ -0,0 +1,120 @@
1
+ require 'spec_helper'
2
+ require 'ostruct'
3
+
4
+ module EndState
5
+ describe Transition do
6
+ subject(:transition) { Transition.new(state) }
7
+ let(:state) { :a }
8
+
9
+ describe '#custom_action' do
10
+ let(:custom) { double :custom }
11
+
12
+ it 'sets the action' do
13
+ transition.custom_action custom
14
+ expect(transition.action).to eq custom
15
+ end
16
+ end
17
+
18
+ describe '#guard' do
19
+ let(:guard) { double :guard }
20
+
21
+ it 'adds a guard' do
22
+ expect { transition.guard guard }.to change(transition.guards, :count).by(1)
23
+ end
24
+
25
+ context 'when params are provided' do
26
+ let(:params) { {} }
27
+
28
+ it 'adds a guard' do
29
+ expect { transition.guard guard, params }.to change(transition.guards, :count).by(1)
30
+ end
31
+ end
32
+ end
33
+
34
+ describe '#allowed?' do
35
+ let(:guard) { double :guard, new: guard_instance }
36
+ let(:guard_instance) { double :guard_instance, allowed?: nil }
37
+ before { transition.guards << { guard: guard, params: {} } }
38
+
39
+ context 'when all guards pass' do
40
+ let(:object) { double :object }
41
+ before { guard_instance.stub(:allowed?).and_return(true) }
42
+
43
+ specify { expect(transition.allowed? object).to be_true }
44
+ end
45
+
46
+ context 'when not all guards pass' do
47
+ let(:object) { double :object }
48
+ before { guard_instance.stub(:allowed?).and_return(false) }
49
+
50
+ specify { expect(transition.allowed? object).to be_false }
51
+ end
52
+ end
53
+
54
+ describe '#will_allow?' do
55
+ let(:guard) { double :guard, new: guard_instance }
56
+ let(:guard_instance) { double :guard_instance, will_allow?: nil }
57
+ before { transition.guards << { guard: guard, params: {} } }
58
+
59
+ context 'when all guards pass' do
60
+ let(:object) { double :object }
61
+ before { guard_instance.stub(:will_allow?).and_return(true) }
62
+
63
+ specify { expect(transition.will_allow? object).to be_true }
64
+ end
65
+
66
+ context 'when not all guards pass' do
67
+ let(:object) { double :object }
68
+ before { guard_instance.stub(:will_allow?).and_return(false) }
69
+
70
+ specify { expect(transition.will_allow? object).to be_false }
71
+ end
72
+ end
73
+
74
+ describe '#finalizer' do
75
+ let(:finalizer) { double :finalizer }
76
+
77
+ it 'adds a finalizer' do
78
+ expect { transition.finalizer finalizer }.to change(transition.finalizers, :count).by(1)
79
+ end
80
+
81
+ context 'when params are provided' do
82
+ let(:params) { {} }
83
+
84
+ it 'adds a finalizer' do
85
+ expect { transition.finalizer finalizer, params }.to change(transition.finalizers, :count).by(1)
86
+ end
87
+ end
88
+ end
89
+
90
+ describe '#persistence_on' do
91
+ it 'adds a Persistence finalizer' do
92
+ expect { transition.persistence_on }.to change(transition.finalizers, :count).by(1)
93
+ end
94
+ end
95
+
96
+ describe '#finalize' do
97
+ let(:finalizer) { double :finalizer, new: finalizer_instance }
98
+ let(:finalizer_instance) { double :finalizer_instance, call: nil, rollback: nil }
99
+ let(:object) { OpenStruct.new(state: :b) }
100
+ before { transition.finalizers << { finalizer: finalizer, params: {} } }
101
+
102
+ context 'when all finalizers succeed' do
103
+ before { finalizer_instance.stub(:call).and_return(true) }
104
+
105
+ specify { expect(transition.finalize object, :a).to be_true }
106
+ end
107
+
108
+ context 'when not all finalizers succeed' do
109
+ before { finalizer_instance.stub(:call).and_return(false) }
110
+
111
+ specify { expect(transition.finalize object, :a).to be_false }
112
+
113
+ it 'rolls them back' do
114
+ transition.finalize object, :a
115
+ expect(finalizer_instance).to have_received(:rollback)
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,4 @@
1
+ require 'spec_helper'
2
+
3
+ describe EndState do
4
+ end
@@ -0,0 +1,2 @@
1
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+ require 'end_state'
metadata ADDED
@@ -0,0 +1,133 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: end_state
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - alexpeachey
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-04-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.5'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.5'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: A modular state machine with single responsibilities.
70
+ email:
71
+ - alex.peachey@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".gitignore"
77
+ - ".hound.yml"
78
+ - ".rspec"
79
+ - ".rubocop.yml"
80
+ - ".travis.yml"
81
+ - Gemfile
82
+ - LICENSE.txt
83
+ - README.md
84
+ - Rakefile
85
+ - end_state.gemspec
86
+ - examples/example1.rb
87
+ - lib/end_state.rb
88
+ - lib/end_state/action.rb
89
+ - lib/end_state/errors.rb
90
+ - lib/end_state/finalizer.rb
91
+ - lib/end_state/finalizers.rb
92
+ - lib/end_state/finalizers/persistence.rb
93
+ - lib/end_state/guard.rb
94
+ - lib/end_state/state_machine.rb
95
+ - lib/end_state/transition.rb
96
+ - lib/end_state/version.rb
97
+ - spec/end_state/action_spec.rb
98
+ - spec/end_state/finalizers/persistence_spec.rb
99
+ - spec/end_state/state_machine_spec.rb
100
+ - spec/end_state/transition_spec.rb
101
+ - spec/end_state_spec.rb
102
+ - spec/spec_helper.rb
103
+ homepage: https://github.com/Originate/end_state
104
+ licenses:
105
+ - MIT
106
+ metadata: {}
107
+ post_install_message:
108
+ rdoc_options: []
109
+ require_paths:
110
+ - lib
111
+ required_ruby_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ version: '0'
116
+ required_rubygems_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ requirements: []
122
+ rubyforge_project:
123
+ rubygems_version: 2.2.2
124
+ signing_key:
125
+ specification_version: 4
126
+ summary: A State Machine implementation
127
+ test_files:
128
+ - spec/end_state/action_spec.rb
129
+ - spec/end_state/finalizers/persistence_spec.rb
130
+ - spec/end_state/state_machine_spec.rb
131
+ - spec/end_state/transition_spec.rb
132
+ - spec/end_state_spec.rb
133
+ - spec/spec_helper.rb