end_state 0.12.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b964eec779e3cb4ab2214bfaf0da1b048f4e9b43
4
- data.tar.gz: c48a186f6bf7229369818e1faf01a46d287dcee3
3
+ metadata.gz: 4431610a8257edd10bd8ba72d453f7f6f318c19b
4
+ data.tar.gz: 3ed496c73b2935568bc12571c0c7d1982446d92f
5
5
  SHA512:
6
- metadata.gz: aca9ba0ae25ef8eb95c988a157ebe487681225feef8785605c2b7414b01725f5b2b733b3fee830b26d65135ccf75e67bb47db6da411b679984ec46dc815f9bc7
7
- data.tar.gz: 8b92ab95a97116056c9aa2331410ebf0ba1a29e641e73345ce5cc9c33ba83cd5e110b4752a5eaab3cf244a7137a39288f01aaf1df670f76dd837fa9eae92ac7a
6
+ metadata.gz: bdea6ed04d7c9e582264e60ed287c5d7fca801f58e1a81732aaca61e80f0ec50d773cd8a54feb3aa582f5562be6b5ed9be2615ac2faa6765d8939ac9a6dc7b1f
7
+ data.tar.gz: 1157ea3e2e91b2406fa9377b2ed44d86bb35015ae75e20bc405aeb58145267d9d72806e8653f4fbd5f26bf4fb8245f7dda44313e4166caf542b5963a2d769f11
data/.travis.yml CHANGED
@@ -1,3 +1,3 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 2.1.1
3
+ - 2.1
data/Gemfile CHANGED
@@ -2,4 +2,5 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in end_state.gemspec
4
4
  gemspec
5
+ gem 'coveralls', require: false
5
6
  gem 'ruby-graphviz'
data/README.md CHANGED
@@ -1,3 +1,7 @@
1
+ [![Build Status](https://travis-ci.org/Originate/end_state.svg?branch=master)](https://travis-ci.org/Originate/end_state)
2
+ [![Code Climate](https://codeclimate.com/github/Originate/end_state/badges/gpa.svg)](https://codeclimate.com/github/Originate/end_state)
3
+ [![Coverage Status](https://coveralls.io/repos/Originate/end_state/badge.png)](https://coveralls.io/r/Originate/end_state)
4
+
1
5
  # EndState
2
6
 
3
7
  EndState is an unobtrusive way to add state machines to your application.
@@ -32,8 +36,8 @@ Transitions can be named by adding an `:as` option.
32
36
  class Machine < EndState::StateMachine
33
37
  transition parked: :idling, as: :start
34
38
  transition idling: :first_gear, first_gear: :second_gear, second_gear: :third_gear, as: :shift_up
35
- transition third_gear: :second_gear, second_gear: :first_gear, as: shift_down
36
- transition first_gear: :idling, as: idle
39
+ transition third_gear: :second_gear, second_gear: :first_gear, as: :shift_down
40
+ transition first_gear: :idling, as: :idle
37
41
  transition [:idling, :first_gear] => :parked, as: :park
38
42
  end
39
43
  ```
@@ -239,17 +243,12 @@ end
239
243
  ## Events
240
244
 
241
245
  By using the `as` option in a transition definition you are creating an event representing that transition.
242
- This can allow you to exercise the machine in a more natural "verb" style interaction. When using `as` event
243
- definitions you can optionally set a `blocked` message on the transition. When the event is executed, if the
244
- machine is not in the initial state of the event, the message is added to the `failure_messages`
245
- array on the machine. Events, like `transition` have both a standard and a bang (`!`) style. The bang style
246
- will raise an exception if there is a problem.
246
+ This can allow you to exercise the machine in a more natural "verb" style interaction. Events, like `transition`
247
+ have both a standard and a bang (`!`) style. The bang style will raise an exception if there is a problem.
247
248
 
248
249
  ```ruby
249
250
  class Machine < EndState::StateMachine
250
- transition a: :b, as: :go do |t|
251
- t.blocked 'Cannot go!'
252
- end
251
+ transition a: :b, as: :go
253
252
  end
254
253
 
255
254
  machine = Machine.new(StatefulObject.new(:a))
@@ -257,7 +256,6 @@ machine = Machine.new(StatefulObject.new(:a))
257
256
  machine.go # => true
258
257
  machine.state # => :b
259
258
  machine.go # => false
260
- machine.failure_messages # => ['Cannot go!']
261
259
  machine.go! # => raises InvalidTransition
262
260
  ```
263
261
 
data/end_state.gemspec CHANGED
@@ -22,4 +22,5 @@ Gem::Specification.new do |spec|
22
22
  spec.add_development_dependency 'rake'
23
23
  spec.add_development_dependency 'rspec'
24
24
  spec.add_development_dependency 'rubocop'
25
+ spec.add_development_dependency 'simplecov'
25
26
  end
@@ -14,7 +14,6 @@ module EndState
14
14
  end
15
15
 
16
16
  def rollback
17
- true
18
17
  end
19
18
  end
20
19
  end
@@ -10,19 +10,23 @@ module EndState
10
10
  end
11
11
 
12
12
  def draw
13
- machine.transitions.keys.each do |t|
14
- left, right = t.to_a.flatten
15
- nodes[left] ||= add_nodes(left.to_s)
16
- nodes[right] ||= add_nodes(right.to_s)
17
- edge = add_edges nodes[left], nodes[right]
18
- if event_labels
19
- event = machine.events.detect do |event, transition|
20
- transition.include? t
21
- end
22
- edge[:label] = event.first.to_s if event
23
- end
24
- end
13
+ add_transitions
25
14
  self
26
15
  end
16
+
17
+ private
18
+
19
+ def add_transitions
20
+ machine.transition_configurations.each do |start_state, end_state, _, event|
21
+ add_transition(start_state, end_state, event)
22
+ end
23
+ end
24
+
25
+ def add_transition start_state, end_state, event
26
+ nodes[start_state] ||= add_node(start_state.to_s)
27
+ nodes[end_state] ||= add_node(end_state.to_s)
28
+ edge = add_edge nodes[start_state], nodes[end_state]
29
+ edge[:label] = event.to_s if event && event_labels
30
+ end
27
31
  end
28
32
  end
@@ -0,0 +1,12 @@
1
+ module EndState
2
+ class Graph
3
+ def initialize(machine, event_labels=true)
4
+ @machine = machine
5
+ @event_labels = event_labels
6
+ end
7
+
8
+ def draw
9
+ self
10
+ end
11
+ end
12
+ end
@@ -1,139 +1,78 @@
1
1
  module EndState
2
2
  class StateMachine < SimpleDelegator
3
+ extend StateMachineConfiguration
4
+
3
5
  attr_accessor :failure_messages, :success_messages
4
6
 
5
7
  def initialize(object)
6
8
  super
7
- Action.new(self, self.class.initial_state).call if self.state.nil?
8
- end
9
-
10
- @initial_state = :__nil__
11
- @mode = :soft
12
-
13
- def self.initial_state
14
- @initial_state
15
- end
16
-
17
- def self.set_initial_state(state)
18
- @initial_state = state.to_sym
19
- end
20
-
21
- def self.treat_all_transitions_as_hard!
22
- @mode = :hard
23
- end
24
-
25
- def self.mode
26
- @mode
27
- end
28
-
29
- def self.store_states_as_strings!
30
- @store_states_as_strings = true
9
+ Action.new(self, self.class.initial_state).call if state.nil?
31
10
  end
32
11
 
33
- def self.store_states_as_strings
34
- !!@store_states_as_strings
12
+ def object
13
+ __getobj__
35
14
  end
36
15
 
37
- def self.transition(state_map)
38
- transition_alias = state_map.delete(:as)
39
- transition_alias = transition_alias.to_sym unless transition_alias.nil?
40
-
41
- state_map.each do |start_states, end_state|
42
- transition = Transition.new(end_state)
43
-
44
- Array(start_states).each do |start_state|
45
- state_mapping = StateMapping[start_state.to_sym => end_state.to_sym]
46
- transitions[state_mapping] = transition
47
- __sm_add_event(transition_alias, state_mapping) unless transition_alias.nil?
48
- end
49
-
50
- yield transition if block_given?
51
- end
16
+ def can_transition?(end_state, params = {})
17
+ return false unless __sm_transition_configuration_for(state, end_state)
18
+ __sm_transition_for(end_state).will_allow?(params)
52
19
  end
53
20
 
54
- def self.transitions
55
- @transitions ||= {}
21
+ def transition(end_state, params = {}, mode = self.class.mode)
22
+ __sm_reset_messages
23
+ return __sm_block_transistion(end_state, mode) unless __sm_transition_configuration_for(state, end_state)
24
+ __sm_transition_for(end_state, mode).call(params)
56
25
  end
57
26
 
58
- def self.events
59
- @events ||= {}
27
+ def transition!(end_state, params = {})
28
+ transition end_state, params, :hard
60
29
  end
61
30
 
62
- def self.state_attribute(attribute)
63
- define_method(:state) { send(attribute.to_sym) }
64
- define_method(:state=) { |val| send("#{attribute}=".to_sym, val) }
31
+ def method_missing(method, *args, &block)
32
+ return super unless __sm_is_method?(method)
33
+ return __sm_predicate(method) if __sm_is_state_predicate?(method)
34
+ __sm_event_transition __sm_event(method), args[0] || {}, __sm_event_mode(method)
65
35
  end
66
36
 
67
- def self.states
68
- (start_states + end_states).uniq
69
- end
37
+ private
70
38
 
71
- def self.start_states
72
- transitions.keys.map(&:start_state).uniq
39
+ def __sm_is_method?(method)
40
+ __sm_is_state_predicate?(method) || __sm_is_event?(method)
73
41
  end
74
42
 
75
- def self.end_states
76
- transitions.keys.map(&:end_state).uniq
43
+ def __sm_predicate(method)
44
+ __sm_current_state? __sm_extract_state(method)
77
45
  end
78
46
 
79
- def object
80
- __getobj__
47
+ def __sm_extract_state(method)
48
+ method.to_s[0..-2].to_sym
81
49
  end
82
50
 
83
- def can_transition?(state, params = {})
84
- previous_state = self.state.to_sym
85
- state = state.to_sym
86
- transition = __sm_transition_for(previous_state, state)
87
- return __sm_block_transistion(transition, state, :soft) unless transition
88
- transition.will_allow? state, params
89
- end
90
-
91
- def transition(state, params = {}, mode = self.class.mode)
92
- @failure_messages = []
93
- @success_messages = []
94
- previous_state = self.state ? self.state.to_sym : self.state
95
- state = state.to_sym
96
- transition = __sm_transition_for(previous_state, state)
97
- mode = __sm_actual_mode(mode)
98
- return __sm_block_transistion(transition, state, mode) unless transition
99
- return __sm_guard_failed(state, mode) unless transition.allowed?(self, params)
100
- return false unless transition.action.new(self, state).call
101
- return __sm_conclude_failed(state, mode) unless transition.conclude(self, previous_state, params)
102
- true
51
+ def __sm_event(method)
52
+ method.to_s.gsub('!','').to_sym
103
53
  end
104
54
 
105
- def transition!(state, params = {})
106
- transition state, params, :hard
55
+ def __sm_event_mode(method)
56
+ return :hard if method.to_s.end_with?('!')
57
+ __sm_actual_mode(:soft)
107
58
  end
108
59
 
109
- def method_missing(method, *args, &block)
110
- return super unless __sm_predicate_or_event?(method)
111
- return __sm_current_state?(method) if __sm_state_predicate(method)
112
- new_state, mode = __sm_event(method)
113
- return false if new_state == :__invalid_event__
114
- transition new_state, (args[0] || {}), mode
60
+ def __sm_is_state_predicate?(method)
61
+ method.to_s.end_with?('?') && self.class.states.include?(__sm_extract_state(method))
115
62
  end
116
63
 
117
- private
118
-
119
- def __sm_predicate_or_event?(method)
120
- __sm_state_predicate(method) ||
121
- __sm_event(method)
64
+ def __sm_is_event?(method)
65
+ self.class.events.include? __sm_event(method)
122
66
  end
123
67
 
124
- def __sm_state_predicate(method)
125
- state = method.to_s[0..-2].to_sym
126
- return unless self.class.states.include?(state) && method.to_s.end_with?('?')
127
- state
68
+ def __sm_current_state?(end_state)
69
+ state.to_sym == end_state
128
70
  end
129
71
 
130
- def __sm_event(method)
131
- event = __sm_state_for_event(method.to_sym, __sm_actual_mode(:soft))
132
- return event, __sm_actual_mode(:soft) if event
133
- return unless method.to_s.end_with?('!')
134
- event = __sm_state_for_event(method.to_s[0..-2].to_sym, :hard)
135
- return event, :hard if event
136
- nil
72
+ def __sm_event_transition(event, params, mode)
73
+ end_state = __sm_state_for_event(event, mode)
74
+ return false if end_state == :__invalid_event__
75
+ transition end_state, params, mode
137
76
  end
138
77
 
139
78
  def __sm_actual_mode(mode)
@@ -141,32 +80,28 @@ module EndState
141
80
  mode
142
81
  end
143
82
 
144
- def __sm_current_state?(method)
145
- state.to_sym == __sm_state_predicate(method)
146
- end
147
-
148
83
  def __sm_state_for_event(event, mode)
149
- state_mappings = self.class.events[event]
150
- return false unless state_mappings
151
- state_mappings.each do |state_mapping|
152
- return state_mapping.end_state if state_mapping.matches_start_state?(state.to_sym)
153
- end
154
- return __sm_invalid_event(event, mode)
84
+ self.class.transition_configurations.get_end_state(state.to_sym, event) || __sm_invalid_event(event, mode)
155
85
  end
156
86
 
157
87
  def __sm_invalid_event(event, mode)
158
88
  fail InvalidTransition, "Transition by event: #{event} is invalid." if mode == :hard
159
- message = self.class.transitions[self.class.events[event].first].blocked_event_message
160
- @failure_messages = [message] if message
161
89
  :__invalid_event__
162
90
  end
163
91
 
164
- def __sm_transition_for(from, to)
165
- self.class.transitions[{ from => to }] ||
166
- self.class.transitions[{ any_state: to }]
92
+ def __sm_transition_configuration_for(start_state, end_state)
93
+ self.class.transition_configurations.get_configuration(start_state, end_state)
167
94
  end
168
95
 
169
- def __sm_block_transistion(transition, state, mode)
96
+ def __sm_transition_for(end_state, mode = self.class.mode)
97
+ start_state = state.to_sym
98
+ end_state = end_state.to_sym
99
+ configuration = __sm_transition_configuration_for(start_state, end_state)
100
+ mode = __sm_actual_mode(mode)
101
+ Transition.new(self, start_state, end_state, configuration, mode)
102
+ end
103
+
104
+ def __sm_block_transistion(state, mode)
170
105
  if self.class.end_states.include? state
171
106
  fail InvalidTransition, "The transition: #{object.state} => #{state} is invalid." if mode == :hard
172
107
  return false
@@ -174,29 +109,9 @@ module EndState
174
109
  fail UnknownState, "The state: #{state} is unknown."
175
110
  end
176
111
 
177
- def __sm_guard_failed(state, mode)
178
- return false unless mode == :hard
179
- fail GuardFailed, "The transition to #{state} was blocked: #{failure_messages.join(', ')}"
180
- end
181
-
182
- def __sm_conclude_failed(state, mode)
183
- return false unless mode == :hard
184
- fail ConcluderFailed, "The transition to #{state} was rolled back: #{failure_messages.join(', ')}"
185
- end
186
-
187
- def self.__sm_add_event(event, state_mapping)
188
- events[event] ||= []
189
- conflicting_mapping = events[event].find{ |sm| sm.conflicts?(state_mapping) }
190
- if conflicting_mapping
191
- message =
192
- "Attempting to define :#{event} as transitioning from " \
193
- ":#{state_mapping.start_state} => :#{state_mapping.end_state} when " \
194
- ":#{conflicting_mapping.start_state} => :#{conflicting_mapping.end_state} already exists. " \
195
- "You cannot define multiple transitions from a single state with the same event name."
196
-
197
- fail EventConflict, message
198
- end
199
- events[event] << state_mapping
112
+ def __sm_reset_messages
113
+ @failure_messages = []
114
+ @success_messages = []
200
115
  end
201
116
  end
202
117
  end
@@ -0,0 +1,77 @@
1
+ module EndState
2
+ module StateMachineConfiguration
3
+ @initial_state = :__nil__
4
+ @mode = :soft
5
+
6
+ def initial_state
7
+ @initial_state
8
+ end
9
+
10
+ def set_initial_state(state)
11
+ @initial_state = state.to_sym
12
+ end
13
+
14
+ def treat_all_transitions_as_hard!
15
+ @mode = :hard
16
+ end
17
+
18
+ def mode
19
+ @mode
20
+ end
21
+
22
+ def store_states_as_strings!
23
+ @store_states_as_strings = true
24
+ end
25
+
26
+ def store_states_as_strings
27
+ !!@store_states_as_strings
28
+ end
29
+
30
+ def transition(state_map)
31
+ event = state_map.delete(:as)
32
+ event = event.to_sym unless event.nil?
33
+
34
+ configuration = TransitionConfiguration.new
35
+ yield configuration if block_given?
36
+
37
+ state_map.each do |start_states, end_state|
38
+ Array(start_states).each do |start_state|
39
+ prevent_event_conflicts(start_state, event)
40
+ transition_configurations.add(start_state, end_state, configuration, event)
41
+ end
42
+ end
43
+ end
44
+
45
+ def transition_configurations
46
+ @transition_configurations ||= TransitionConfigurationSet.new
47
+ end
48
+
49
+ def state_attribute(attribute)
50
+ define_method(:state) { send(attribute.to_sym) }
51
+ define_method(:state=) { |val| send("#{attribute}=".to_sym, val) }
52
+ end
53
+
54
+ def events
55
+ transition_configurations.events
56
+ end
57
+
58
+ def states
59
+ (start_states + end_states).uniq
60
+ end
61
+
62
+ def start_states
63
+ transition_configurations.start_states
64
+ end
65
+
66
+ def end_states
67
+ transition_configurations.end_states
68
+ end
69
+
70
+ private
71
+
72
+ def prevent_event_conflicts(start_state, event)
73
+ return unless transition_configurations.event_conflicts?(start_state, event)
74
+ fail EventConflict, "Attempting to define event '#{event}' on state '#{start_state}', but it is already defined. (Check duplicates and use of 'any_state')"
75
+ end
76
+ end
77
+ end
@@ -1,82 +1,68 @@
1
1
  module EndState
2
2
  class Transition
3
- attr_reader :state, :blocked_event_message
4
- attr_accessor :action, :guards, :concluders, :allowed_params, :required_params
3
+ attr_reader :configuration, :mode, :object, :previous_state, :state
5
4
 
6
- def initialize(state)
5
+ def initialize(object, previous_state, state, configuration, mode)
6
+ @object = object
7
+ @previous_state = previous_state
7
8
  @state = state
8
- @action = Action
9
- @guards = []
10
- @concluders = []
11
- @allowed_params = []
12
- @required_params = []
9
+ @configuration = configuration
10
+ @mode = mode
13
11
  end
14
12
 
15
- def allowed?(object, params={})
13
+ def call(params={})
14
+ return guard_failed unless allowed?(params)
15
+ return false unless action.new(object, state).call
16
+ return conclude_failed unless conclude(params)
17
+ true
18
+ end
19
+
20
+ def allowed?(params={})
16
21
  raise "Missing params: #{missing_params(params).join(',')}" unless missing_params(params).empty?
17
22
  guards.all? { |guard| guard.new(object, state, params).allowed? }
18
23
  end
19
24
 
20
- def will_allow?(object, params={})
25
+ def will_allow?(params={})
21
26
  return false unless missing_params(params).empty?
22
27
  guards.all? { |guard| guard.new(object, state, params).will_allow? }
23
28
  end
24
29
 
25
- def conclude(object, previous_state, params={})
26
- concluders.each_with_object([]) do |concluder, concluded|
27
- concluded << concluder
28
- return rollback(concluded, object, previous_state, params) unless run_concluder(concluder, object, state, params)
29
- end
30
- true
31
- end
32
-
33
- def custom_action(action)
34
- @action = action
35
- end
30
+ private
36
31
 
37
- def guard(*guards)
38
- Array(guards).flatten.each { |guard| self.guards << guard }
32
+ def failed(error, message)
33
+ return false unless mode == :hard
34
+ fail error, "The transition to #{state} was #{message}: #{object.failure_messages.join(', ')}"
39
35
  end
40
36
 
41
- def concluder(*concluders)
42
- Array(concluders).flatten.each { |concluder| self.concluders << concluder }
37
+ def guard_failed
38
+ failed GuardFailed, 'blocked'
43
39
  end
44
40
 
45
- def persistence_on
46
- concluder Concluders::Persistence
41
+ def conclude_failed
42
+ failed ConcluderFailed, 'rolled back'
47
43
  end
48
44
 
49
- def allow_params(*params)
50
- Array(params).flatten.each do |param|
51
- self.allowed_params << param unless self.allowed_params.include? param
52
- end
53
- end
54
-
55
- def require_params(*params)
56
- Array(params).flatten.each do |param|
57
- self.allowed_params << param unless self.allowed_params.include? param
58
- self.required_params << param unless self.required_params.include? param
45
+ def conclude(params={})
46
+ concluders.each_with_object([]) do |concluder, concluded|
47
+ concluded << concluder
48
+ return rollback(concluded, params) unless concluder.new(object, state, params).call
59
49
  end
50
+ true
60
51
  end
61
52
 
62
- def blocked(message)
63
- @blocked_event_message = message
64
- end
65
-
66
- private
67
-
68
- def rollback(concluded, object, previous_state, params)
53
+ def rollback(concluded, params)
54
+ concluded.reverse_each { |concluder| concluder.new(object, state, params).rollback }
69
55
  action.new(object, previous_state).rollback
70
- concluded.reverse.each { |concluder| concluder.new(object, state, params).rollback }
71
56
  false
72
57
  end
73
58
 
74
- def run_concluder(concluder, object, state, params)
75
- concluder.new(object, state, params).call
76
- end
77
-
78
59
  def missing_params(params)
79
60
  required_params.select { |key| params[key].nil? }
80
61
  end
62
+
63
+ [:action, :concluders, :guards, :required_params].each do |method|
64
+ define_method(method) { configuration.public_send(method) }
65
+ private method
66
+ end
81
67
  end
82
68
  end
@@ -0,0 +1,49 @@
1
+ module EndState
2
+ class TransitionConfiguration
3
+ attr_reader :action, :allowed_params, :concluders, :guards, :required_params
4
+
5
+ def initialize
6
+ @action = Action
7
+ @allowed_params = []
8
+ @concluders = []
9
+ @guards = []
10
+ @required_params = []
11
+ end
12
+
13
+ def custom_action(action)
14
+ @action = action
15
+ end
16
+
17
+ def guard(*guards)
18
+ Array(guards).flatten.each { |guard| self.guards << guard }
19
+ end
20
+
21
+ def concluder(*concluders)
22
+ Array(concluders).flatten.each { |concluder| self.concluders << concluder }
23
+ end
24
+
25
+ def persistence_on
26
+ concluder Concluders::Persistence
27
+ end
28
+
29
+ def allow_params(*params)
30
+ Array(params).flatten.each do |param|
31
+ append_unless_included(:allowed_params, param)
32
+ end
33
+ end
34
+
35
+ def require_params(*params)
36
+ Array(params).flatten.each do |param|
37
+ append_unless_included(:allowed_params, param)
38
+ append_unless_included(:required_params, param)
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def append_unless_included(name, value)
45
+ attribute = self.send(name)
46
+ attribute << value unless attribute.include? value
47
+ end
48
+ end
49
+ end