end_state 0.12.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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