state_gate 1.2.3
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 +7 -0
- data/lib/state_gate/builder/conflict_detection_methods.rb +134 -0
- data/lib/state_gate/builder/dynamic_module_creation_methods.rb +120 -0
- data/lib/state_gate/builder/scope_methods.rb +96 -0
- data/lib/state_gate/builder/state_methods.rb +289 -0
- data/lib/state_gate/builder/transition_methods.rb +142 -0
- data/lib/state_gate/builder/transition_validation_methods.rb +247 -0
- data/lib/state_gate/builder.rb +244 -0
- data/lib/state_gate/engine/configurator.rb +230 -0
- data/lib/state_gate/engine/errator.rb +63 -0
- data/lib/state_gate/engine/fixer.rb +75 -0
- data/lib/state_gate/engine/scoper.rb +66 -0
- data/lib/state_gate/engine/sequencer.rb +116 -0
- data/lib/state_gate/engine/stator.rb +225 -0
- data/lib/state_gate/engine/transitioner.rb +73 -0
- data/lib/state_gate/engine.rb +65 -0
- data/lib/state_gate/locale/builder_en.yml +14 -0
- data/lib/state_gate/locale/engine_en.yml +58 -0
- data/lib/state_gate/locale/state_gate_en.yml +5 -0
- data/lib/state_gate/rspec/allow_transitions_on.rb +259 -0
- data/lib/state_gate/rspec/have_states.rb +140 -0
- data/lib/state_gate/rspec.rb +6 -0
- data/lib/state_gate/type.rb +112 -0
- data/lib/state_gate.rb +193 -0
- metadata +83 -0
@@ -0,0 +1,225 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module StateGate
|
4
|
+
class Engine
|
5
|
+
##
|
6
|
+
# = Description
|
7
|
+
#
|
8
|
+
# Provides state helper methods for StateGate::Engine.
|
9
|
+
#
|
10
|
+
module Stator
|
11
|
+
|
12
|
+
# ======================================================================
|
13
|
+
# Configursation Methods
|
14
|
+
# ======================================================================
|
15
|
+
|
16
|
+
# Add a new +state+.
|
17
|
+
#
|
18
|
+
# [:state_name*]
|
19
|
+
# The name for the new state. (Symbol | required)
|
20
|
+
# state :state_name
|
21
|
+
#
|
22
|
+
#
|
23
|
+
# [:transitions_to]
|
24
|
+
# A list of other state that this state may change to. (Array | optional)
|
25
|
+
# state :state_name, transtions_to: [:state_1, :state_4, :state_5]
|
26
|
+
#
|
27
|
+
# [:human]
|
28
|
+
# A display name for the state used in views.
|
29
|
+
# (String | optoinal | default: state.titleized)
|
30
|
+
# state :state_name, transtions_to: [:state_1, :state_4, :state_5], human: "My State"
|
31
|
+
#
|
32
|
+
def state(name, opts = {})
|
33
|
+
name = StateGate.symbolize(name)
|
34
|
+
assert_valid_state_name(name)
|
35
|
+
assert_valid_opts(opts, name)
|
36
|
+
|
37
|
+
# add the state
|
38
|
+
@states[name] = state_template.merge({ human: opts[:human] || name.to_s.titleize })
|
39
|
+
|
40
|
+
# add the state transitions
|
41
|
+
Array(opts[:transitions_to]).each do |transition|
|
42
|
+
@states[name][:transitions_to] << StateGate.symbolize(transition)
|
43
|
+
end
|
44
|
+
end # state
|
45
|
+
|
46
|
+
|
47
|
+
|
48
|
+
# The name for the default state for a new object. (String | required)
|
49
|
+
#
|
50
|
+
# default :state_name
|
51
|
+
#
|
52
|
+
def default(val = nil)
|
53
|
+
cerr(:default_type_err, kattr: true) unless val.is_a?(Symbol)
|
54
|
+
cerr(:default_repeat_err, kattr: true) if @default
|
55
|
+
@default = StateGate.symbolize(val)
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
|
60
|
+
# ======================================================================
|
61
|
+
# Helper Methods
|
62
|
+
# ======================================================================
|
63
|
+
|
64
|
+
##
|
65
|
+
# Returns an Array defined states.
|
66
|
+
#
|
67
|
+
# .states # => [:pending, :active, :suspended, :archived]
|
68
|
+
#
|
69
|
+
def states
|
70
|
+
@states.keys
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
|
75
|
+
##
|
76
|
+
# Ensures the given value is a valid state name.
|
77
|
+
#
|
78
|
+
# [value]
|
79
|
+
# A String or Symbol state name.
|
80
|
+
#
|
81
|
+
# .assert_valid_state!(:active) # => :active
|
82
|
+
# .assert_valid_state!('PENDING') # => :pending
|
83
|
+
# .assert_valid_state!(:dummy) # => ArgumentError
|
84
|
+
#
|
85
|
+
#
|
86
|
+
# [Note]
|
87
|
+
# Valid state names preceeded with +force_+ are also allowed.
|
88
|
+
#
|
89
|
+
# .assert_valid_state!(:force_active) # => :force_active
|
90
|
+
#
|
91
|
+
# Returns the Symbol state name
|
92
|
+
# Raises an exception if the value is not a valid state name.
|
93
|
+
#
|
94
|
+
def assert_valid_state!(value)
|
95
|
+
state_name = StateGate.symbolize(value)
|
96
|
+
unforced_state = state_name.to_s.remove(/^force_/).to_sym
|
97
|
+
invalid_state_error(value) unless @states.keys.include?(unforced_state)
|
98
|
+
state_name
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
|
103
|
+
##
|
104
|
+
# Returns an Array of the human display names for each defined state.
|
105
|
+
#
|
106
|
+
# human_states # => ['Pending Activation', 'Active', 'Suspended By Admin']
|
107
|
+
#
|
108
|
+
def human_states
|
109
|
+
@states.map { |_k, v| v[:human] }
|
110
|
+
end
|
111
|
+
|
112
|
+
|
113
|
+
|
114
|
+
##
|
115
|
+
# Returns the human display name for a given state.
|
116
|
+
#
|
117
|
+
# .human_state_for(:pending) # => 'Panding Activation'
|
118
|
+
# .human_state_for(:active) # => 'Active'
|
119
|
+
#
|
120
|
+
def human_state_for(state)
|
121
|
+
state_name = assert_valid_state!(state)
|
122
|
+
@states[state_name][:human]
|
123
|
+
end
|
124
|
+
|
125
|
+
|
126
|
+
|
127
|
+
##
|
128
|
+
# Return an Array of states, with their human display names, ready for
|
129
|
+
# use in a form select.
|
130
|
+
#
|
131
|
+
# sorted - TRUE is the states should be sorted by human name, defaults to false
|
132
|
+
#
|
133
|
+
# .states_for_select # => [['Pending Activation', 'pending'],
|
134
|
+
# # ['Active', 'active'],
|
135
|
+
# # ['Suspended by Admin', 'suspended']]
|
136
|
+
#
|
137
|
+
# .states_for_select(true) # => [['Active', 'active'],
|
138
|
+
# # ['Pending Activation', 'pending'],
|
139
|
+
# # ['Suspended by Admin', 'suspended']]
|
140
|
+
#
|
141
|
+
def states_for_select(sorted = false)
|
142
|
+
result = []
|
143
|
+
if sorted
|
144
|
+
@states.sort_by { |_k, v| v[:human] }
|
145
|
+
.each { |state, opts| result << [opts[:human], state.to_s] }
|
146
|
+
else
|
147
|
+
@states.each { |state, opts| result << [opts[:human], state.to_s] }
|
148
|
+
end
|
149
|
+
result
|
150
|
+
end
|
151
|
+
|
152
|
+
|
153
|
+
|
154
|
+
##
|
155
|
+
# Return the raw states hash, allowing inspection of the core engine states.
|
156
|
+
#
|
157
|
+
# .raw_states # => { pending: { transitions_to: [:active],
|
158
|
+
# # human: 'Pending Activation'},
|
159
|
+
# # active: { transitions_to: [:pending],
|
160
|
+
# # human: 'Active'}}
|
161
|
+
#
|
162
|
+
def raw_states
|
163
|
+
@states
|
164
|
+
end
|
165
|
+
|
166
|
+
|
167
|
+
|
168
|
+
##
|
169
|
+
# Returns the state_gate default state
|
170
|
+
#
|
171
|
+
# .default_state # => :pending
|
172
|
+
#
|
173
|
+
def default_state
|
174
|
+
@default
|
175
|
+
end
|
176
|
+
|
177
|
+
|
178
|
+
|
179
|
+
# ======================================================================
|
180
|
+
# Private
|
181
|
+
# ======================================================================
|
182
|
+
private
|
183
|
+
|
184
|
+
|
185
|
+
|
186
|
+
# return a Hash with the state keys defined.
|
187
|
+
def state_template
|
188
|
+
{
|
189
|
+
transitions_to: [],
|
190
|
+
previous_state: nil,
|
191
|
+
next_state: nil,
|
192
|
+
scope_name: nil,
|
193
|
+
human: ''
|
194
|
+
}
|
195
|
+
end
|
196
|
+
|
197
|
+
|
198
|
+
|
199
|
+
# fail if the supplied name is not a symbol, already added
|
200
|
+
# or starts with 'not_' or 'force'.
|
201
|
+
#
|
202
|
+
def assert_valid_state_name(name)
|
203
|
+
cerr(:state_type_err, kattr: true) unless name.is_a?(Symbol)
|
204
|
+
cerr(:state_repeat_err, state: name, kattr: true) if @states.key?(name)
|
205
|
+
cerr(:state_not_name_err, state: name, kattr: true) if name.to_s.start_with?('not_')
|
206
|
+
cerr(:state_force_name_err, state: name, kattr: true) if name.to_s.start_with?('force_')
|
207
|
+
end
|
208
|
+
|
209
|
+
|
210
|
+
|
211
|
+
# Fail if opts is not a hash, has non-symbol keys or non-symbol transitions.
|
212
|
+
#
|
213
|
+
def assert_valid_opts(opts, state_name)
|
214
|
+
unless opts.keys.reject { |k| k.is_a?(Symbol) }.blank?
|
215
|
+
cerr(:state_opts_key_type_err, state: state_name, kattr: true)
|
216
|
+
end
|
217
|
+
|
218
|
+
return if Array(opts[:transitions_to]).reject { |k| k.is_a?(Symbol) }.blank?
|
219
|
+
|
220
|
+
cerr(:transition_key_type_err, state: state_name, kattr: true)
|
221
|
+
end
|
222
|
+
|
223
|
+
end # Stater
|
224
|
+
end # Engine
|
225
|
+
end # StateGate
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module StateGate
|
4
|
+
class Engine
|
5
|
+
##
|
6
|
+
# = Description
|
7
|
+
#
|
8
|
+
# Provides transition helper methods for StateGate::Engine.
|
9
|
+
#
|
10
|
+
module Transitioner
|
11
|
+
|
12
|
+
##
|
13
|
+
# Returns TRUE if every state can transition to every other state, rendering
|
14
|
+
# transitions pointless.
|
15
|
+
#
|
16
|
+
# .transitionless? # => true
|
17
|
+
#
|
18
|
+
def transitionless?
|
19
|
+
!!@transitionless
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
|
24
|
+
##
|
25
|
+
# Returns a Hash of states and allowed transitions.
|
26
|
+
#
|
27
|
+
# .transitions # => { pending: [:active],
|
28
|
+
# # ativive: [:suspended, :archived],
|
29
|
+
# # suspended: [:active, :archived],
|
30
|
+
# # archived: [] }
|
31
|
+
#
|
32
|
+
def transitions
|
33
|
+
@transitions ||= begin
|
34
|
+
transitions = {}
|
35
|
+
@states.each { |k, v| transitions[k] = v[:transitions_to] } # rubocop:disable Layout/ExtraSpacing
|
36
|
+
transitions
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
|
42
|
+
##
|
43
|
+
# Return an Array of allowed transitions for the given state
|
44
|
+
#
|
45
|
+
# .transitions_for_state(:active) # => [:suspended, :archived]
|
46
|
+
#
|
47
|
+
def transitions_for_state(state)
|
48
|
+
state_name = assert_valid_state!(state)
|
49
|
+
transitions[state_name]
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
|
54
|
+
##
|
55
|
+
# Returns TRUE if a transition is allowed, otherwise raises an exception.
|
56
|
+
#
|
57
|
+
# .assert_valid_transition!(:pending, :active) # => true
|
58
|
+
# .assert_valid_transition!(:active, :pending) # => ArgumentError
|
59
|
+
#
|
60
|
+
def assert_valid_transition!(current_state = nil, new_state = nil)
|
61
|
+
from_state = assert_valid_state!(current_state)
|
62
|
+
to_state = assert_valid_state!(new_state)
|
63
|
+
|
64
|
+
return true if to_state == from_state
|
65
|
+
return true if to_state.to_s.start_with?('force_')
|
66
|
+
return true if @states[from_state][:transitions_to].include?(to_state)
|
67
|
+
|
68
|
+
aerr(:invalid_state_transition_err, from: from_state, to: to_state, kattr: true)
|
69
|
+
end
|
70
|
+
|
71
|
+
end # Sequencer
|
72
|
+
end # Engine
|
73
|
+
end # StateGate
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Dir[File.join(__dir__, 'engine', '*.rb')].sort.each { |file| require file }
|
4
|
+
|
5
|
+
module StateGate
|
6
|
+
##
|
7
|
+
# = Description
|
8
|
+
#
|
9
|
+
# Contains a list of the _state-gate_ defined states, allowed transitions and configuration
|
10
|
+
# options.
|
11
|
+
#
|
12
|
+
# The engine is queried by all helper methods to validate states and allowed transitions.
|
13
|
+
#
|
14
|
+
class Engine
|
15
|
+
|
16
|
+
include Configurator
|
17
|
+
include Transitioner
|
18
|
+
include Sequencer
|
19
|
+
include Errator
|
20
|
+
include Scoper
|
21
|
+
include Stator
|
22
|
+
include Fixer
|
23
|
+
|
24
|
+
|
25
|
+
|
26
|
+
# ======================================================================
|
27
|
+
# = Private
|
28
|
+
# ======================================================================
|
29
|
+
private
|
30
|
+
|
31
|
+
# Initialize the engine, setting the Class and attribute for the new engine
|
32
|
+
# and parsing the provided configuration.
|
33
|
+
#
|
34
|
+
# StateGate::Engine.new(MyKlass, :status) do
|
35
|
+
# ... configuration ...
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
def initialize(klass, attribute, &config)
|
39
|
+
aerror(:klass_type_err) unless klass.respond_to?(:to_s)
|
40
|
+
aerror(:attribute_type_err) unless attribute.is_a?(Symbol)
|
41
|
+
aerror(:missing_config_block_err) unless block_given?
|
42
|
+
|
43
|
+
@klass = klass
|
44
|
+
@attribute = StateGate.symbolize(attribute)
|
45
|
+
|
46
|
+
set_defaults
|
47
|
+
|
48
|
+
parse_configuration(&config)
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
|
53
|
+
# Set the class variables with default values
|
54
|
+
def set_defaults
|
55
|
+
@states = {}
|
56
|
+
@default = nil
|
57
|
+
@prefix = nil
|
58
|
+
@suffix = nil
|
59
|
+
@scopes = true
|
60
|
+
@sequential = false
|
61
|
+
@transitionless = false
|
62
|
+
end
|
63
|
+
|
64
|
+
end # Engine
|
65
|
+
end # StateGate
|
@@ -0,0 +1,14 @@
|
|
1
|
+
en:
|
2
|
+
state_gate:
|
3
|
+
builder:
|
4
|
+
|
5
|
+
non_class_err: "StateGate requires %{klass} to be a Class."
|
6
|
+
non_active_record_err: "StateGate requires %{klass} to derive from ActiveRecord."
|
7
|
+
missing_attribute_err: "Missing attribute name when using 'state_gate' in class '%{klass}'."
|
8
|
+
attribute_type_err: "StateGate <attr> must be a Symbol."
|
9
|
+
non_db_attr_err: "%{kattr} is not a database attribute."
|
10
|
+
non_string_column_err: "StateGate requires %{kattr} to be a database :string type."
|
11
|
+
existing_state_gate_err: "An StateGate has already been defined for %{kattr}."
|
12
|
+
conflict_err: "StateGate for %{klass}#%{attribute} will generate %{type} method '%{method_name}', which is already defined by %{source}."
|
13
|
+
|
14
|
+
|
@@ -0,0 +1,58 @@
|
|
1
|
+
en:
|
2
|
+
state_gate:
|
3
|
+
engine:
|
4
|
+
|
5
|
+
# ======================================================================
|
6
|
+
# = Engine
|
7
|
+
# ======================================================================
|
8
|
+
|
9
|
+
attribute_type_err: "The provided attribute must be a Symbol."
|
10
|
+
invalid_state_err: "%{val} is not valid state for %{kattr}."
|
11
|
+
klass_type_err: "The proved class must be a Class or String."
|
12
|
+
missing_config_block_err: "No configuration block provided."
|
13
|
+
|
14
|
+
|
15
|
+
|
16
|
+
# ======================================================================
|
17
|
+
# = Engine Configuration
|
18
|
+
# ======================================================================
|
19
|
+
|
20
|
+
config:
|
21
|
+
bad_command: "'%{cmd}' is not a valid configuration option."
|
22
|
+
default_repeat_err: "default for %{kattr} has been specified multiple times."
|
23
|
+
default_state_err: "The default set for %{kattr} is not a valid state."
|
24
|
+
default_type_err: "default for %{kattr} must be a Symbol."
|
25
|
+
prefix_multiple_err: "prefix for %{kattr} has been defined multiple times."
|
26
|
+
prefix_type_err: "prefix for %{kattr} must be a Symbol."
|
27
|
+
single_state_err: "%{kattr} needs at least two states defined."
|
28
|
+
state_force_name_err: "%{kattr}:%{state} states cannot begin with 'force_'."
|
29
|
+
state_not_name_err: "%{kattr}:%{state} states cannot begin with 'not_'."
|
30
|
+
state_opts_key_type_err: "options for %{kattr}:%{state} must be Symbols."
|
31
|
+
state_repeat_err: "%{kattr}:%{state} has been defined multiple times."
|
32
|
+
state_type_err: "states for %{kattr} must be a Symbol."
|
33
|
+
states_missing_err: "no states have been defined for %{kattr}."
|
34
|
+
suffix_multiple_err: "suffix for %{kattr} has been defined multiple times."
|
35
|
+
suffix_type_err: "suffix for %{kattr} must be a Symbol."
|
36
|
+
transition_key_type_err: "transitions for %{kattr}:%{state} must be Symbols."
|
37
|
+
any_transition_err: "when transitioning to :any on %{kattr}:%{state}, :any must be the only transition."
|
38
|
+
transition_state_err: "%{kattr} transitions from :%{state} to invalid state :%{transition}."
|
39
|
+
transitionless_states_err: "There are no state transitions leading to %{kattr} %{states}."
|
40
|
+
|
41
|
+
|
42
|
+
|
43
|
+
# ======================================================================
|
44
|
+
# = Engine Sequencer
|
45
|
+
# ======================================================================
|
46
|
+
|
47
|
+
non_sequential_err: "%{kattr} is not sequential and does not implement sequence methods."
|
48
|
+
|
49
|
+
|
50
|
+
|
51
|
+
# ======================================================================
|
52
|
+
# = Engine Transitioner
|
53
|
+
# ======================================================================
|
54
|
+
|
55
|
+
invalid_state_transition_err: "%{kattr} cannot transition from :%{from} to :%{to}."
|
56
|
+
|
57
|
+
|
58
|
+
|
@@ -0,0 +1,259 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
##
|
4
|
+
# = Description
|
5
|
+
#
|
6
|
+
# RSpec matcher to verify allowed state transitions.
|
7
|
+
#
|
8
|
+
# [:source_obj]
|
9
|
+
# The Class or Instance to be tested.
|
10
|
+
#
|
11
|
+
# [:attr_name]
|
12
|
+
# The attrbute being tested.
|
13
|
+
#
|
14
|
+
# [:from]
|
15
|
+
# The state being transtioned from
|
16
|
+
#
|
17
|
+
# [:to]
|
18
|
+
# The states being transitions to
|
19
|
+
#
|
20
|
+
# expect(User).to allow_transitions_on(:status).from(:active).to(:suspended, :archived)
|
21
|
+
#
|
22
|
+
# Fails if a given transitions is not allowed, or an allowed transition is missing.
|
23
|
+
#
|
24
|
+
RSpec::Matchers.define :allow_transitions_on do |attr_name| # rubocop:disable Metrics/BlockLength
|
25
|
+
##
|
26
|
+
# Expect the given attribute state to match all given transitions.
|
27
|
+
#
|
28
|
+
match do |source_obj| # :nodoc:
|
29
|
+
# validate we have a state engine and parameters
|
30
|
+
return false unless valid_setup?(attr_name, source_obj)
|
31
|
+
|
32
|
+
allowed_transitions = source_obj.stateables[@key]
|
33
|
+
.transitions_for_state(@state)
|
34
|
+
expected_transitions = @to.map(&:to_s).map(&:to_sym)
|
35
|
+
@missing_states = allowed_transitions - expected_transitions
|
36
|
+
@extra_states = expected_transitions - allowed_transitions
|
37
|
+
|
38
|
+
@error = :missing_states if @missing_states.any?
|
39
|
+
@error = :extra_states if @extra_states.any?
|
40
|
+
|
41
|
+
@error ? false : true
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
|
46
|
+
# Expect the attribute state not to have any given transitions.
|
47
|
+
#
|
48
|
+
match_when_negated do |source_obj|
|
49
|
+
# validate we have a state engine and parameters
|
50
|
+
return false unless valid_setup?(attr_name, source_obj)
|
51
|
+
|
52
|
+
allowed_transitions = source_obj.stateables[@key]
|
53
|
+
.transitions_for_state(@state)
|
54
|
+
expected_transitions = @to.map(&:to_s).map(&:to_sym)
|
55
|
+
remaining_states = expected_transitions - allowed_transitions
|
56
|
+
|
57
|
+
unless remaining_states.count == expected_transitions.count
|
58
|
+
@error = :found_states
|
59
|
+
@found_states = expected_transitions - remaining_states
|
60
|
+
end
|
61
|
+
|
62
|
+
@error ? false : true
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
|
67
|
+
# The state to be checked.
|
68
|
+
chain :from do |state|
|
69
|
+
@state = state
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
|
74
|
+
# The transitions to check
|
75
|
+
chain :to do |*transitions|
|
76
|
+
@to = transitions.flatten
|
77
|
+
@to_called = true
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
|
82
|
+
# Failure messages for an expected match.
|
83
|
+
#
|
84
|
+
failure_message do
|
85
|
+
case @error
|
86
|
+
when :no_state_gates
|
87
|
+
"no state machines are defined for #{@source_name}."
|
88
|
+
|
89
|
+
when :invalid_key
|
90
|
+
"no state machine is defined for ##{@key}."
|
91
|
+
|
92
|
+
when :invalid_state
|
93
|
+
":#{@state} is not a valid state for #{@source_name}##{@key}."
|
94
|
+
|
95
|
+
when :no_from
|
96
|
+
'missing ".from(<state>)".'
|
97
|
+
|
98
|
+
when :no_to
|
99
|
+
'missing ".to(<states>)".'
|
100
|
+
|
101
|
+
when :invalid_transition_states
|
102
|
+
states = @invalid_states.map { |s| ":#{s}" }
|
103
|
+
if states.one?
|
104
|
+
"#{states.first} is not a valid ##{@key} state."
|
105
|
+
else
|
106
|
+
"#{states.to_sentence} are not valid ##{@key} states."
|
107
|
+
end
|
108
|
+
|
109
|
+
when :missing_states
|
110
|
+
states = @missing_states.map { |s| ":#{s}" }
|
111
|
+
"##{@key} also transitions from :#{@state} to #{states.to_sentence}."
|
112
|
+
|
113
|
+
when :extra_states
|
114
|
+
states = @extra_states.map { |s| ":#{s}" }
|
115
|
+
"##{@key} does not transition from :#{@state} to #{states.to_sentence}."
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
|
120
|
+
|
121
|
+
# failure messages for a negated match.
|
122
|
+
#
|
123
|
+
failure_message_when_negated do
|
124
|
+
case @error
|
125
|
+
when :no_state_gates
|
126
|
+
"no state machines are defined for #{@source_name}."
|
127
|
+
|
128
|
+
when :invalid_key
|
129
|
+
"no state machine is defined for ##{@key}."
|
130
|
+
|
131
|
+
when :invalid_state
|
132
|
+
":#{@state} is not a valid state for #{@source_name}##{@key}."
|
133
|
+
|
134
|
+
when :no_from
|
135
|
+
'missing ".from(<state>)".'
|
136
|
+
|
137
|
+
when :no_to
|
138
|
+
'missing ".to(<states>)".'
|
139
|
+
|
140
|
+
when :invalid_transition_states
|
141
|
+
states = @invalid_states.map { |s| ":#{s}" }
|
142
|
+
if states.one?
|
143
|
+
"#{states.first} is not a valid ##{@key} state."
|
144
|
+
else
|
145
|
+
"#{states.to_sentence} are not valid ##{@key} states."
|
146
|
+
end
|
147
|
+
|
148
|
+
when :found_states
|
149
|
+
states = @found_states.map { |s| ":#{s}" }
|
150
|
+
":#{@state} is allowed to transition to #{states.to_sentence}."
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
|
155
|
+
|
156
|
+
# = Helpers
|
157
|
+
# ======================================================================
|
158
|
+
|
159
|
+
# Check the setup is correct with the required information available.
|
160
|
+
#
|
161
|
+
def valid_setup?(attr_name, source_obj) # :nodoc:
|
162
|
+
@key = StateGate.symbolize(attr_name)
|
163
|
+
@state = StateGate.symbolize(@state)
|
164
|
+
@source_name = source_obj.is_a?(Class) ? source_obj.name : source_obj.class.name
|
165
|
+
|
166
|
+
# detect_setup_errors(source_obj)
|
167
|
+
|
168
|
+
return false unless assert_state_gate(source_obj)
|
169
|
+
return false unless assert_valid_key(source_obj)
|
170
|
+
return false unless assert_from_present
|
171
|
+
return false unless assert_valid_state
|
172
|
+
return false unless assert_to_present
|
173
|
+
|
174
|
+
assert_valid_transition
|
175
|
+
end
|
176
|
+
|
177
|
+
|
178
|
+
|
179
|
+
# Validate the state machines container exists
|
180
|
+
#
|
181
|
+
def assert_state_gate(source_obj)
|
182
|
+
return true if source_obj.respond_to?(:stateables)
|
183
|
+
|
184
|
+
@error = :no_state_gates
|
185
|
+
false
|
186
|
+
end
|
187
|
+
|
188
|
+
|
189
|
+
|
190
|
+
# Validate the state machine is there
|
191
|
+
#
|
192
|
+
def assert_valid_key(source_obj)
|
193
|
+
@eng = source_obj.stateables[@key]
|
194
|
+
return true unless @eng.blank?
|
195
|
+
|
196
|
+
@error = :invalid_key
|
197
|
+
false
|
198
|
+
end
|
199
|
+
|
200
|
+
|
201
|
+
|
202
|
+
# Validate the :from state is present
|
203
|
+
#
|
204
|
+
def assert_from_present
|
205
|
+
return true unless @state.blank?
|
206
|
+
|
207
|
+
@error = :no_from
|
208
|
+
false
|
209
|
+
end
|
210
|
+
|
211
|
+
|
212
|
+
|
213
|
+
# Validate it is a valid state supplied
|
214
|
+
#
|
215
|
+
def assert_valid_state
|
216
|
+
return true if @eng.states.include?(@state)
|
217
|
+
|
218
|
+
@error = :invalid_state
|
219
|
+
false
|
220
|
+
end
|
221
|
+
|
222
|
+
|
223
|
+
|
224
|
+
# Validate the transitions have been supplied
|
225
|
+
#
|
226
|
+
def assert_to_present
|
227
|
+
return true if @to_called
|
228
|
+
|
229
|
+
@error = :no_to
|
230
|
+
false
|
231
|
+
end
|
232
|
+
|
233
|
+
|
234
|
+
|
235
|
+
# Validate the supplied transitions are valid
|
236
|
+
#
|
237
|
+
def assert_valid_transition
|
238
|
+
return true unless invalid_transition_states?
|
239
|
+
|
240
|
+
@error = :invalid_transition_states
|
241
|
+
false
|
242
|
+
end
|
243
|
+
|
244
|
+
|
245
|
+
|
246
|
+
# Check the supplied transition states are valid for the attribute.
|
247
|
+
#
|
248
|
+
def invalid_transition_states? # :nodoc:
|
249
|
+
@invalid_states = []
|
250
|
+
@to.each do |state|
|
251
|
+
unless @eng.states.include?(state.to_s.to_sym)
|
252
|
+
@invalid_states << state.to_s.to_sym
|
253
|
+
@error = :invalid_transition_states
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
@invalid_states.any? ? true : false
|
258
|
+
end
|
259
|
+
end
|