Top Level Namespace

Defined Under Namespace

Modules: StateGate

Instance Method Summary collapse

Instance Method Details

#allow_transitions_onObject

Description

RSpec matcher to verify allowed state transitions.

:source_obj

The Class or Instance to be tested.

:attr_name

The attrbute being tested.

:from

The state being transtioned from

:to

The states being transitions to

expect(User).to allow_transitions_on(:status).from(:active).to(:suspended, :archived)

Fails if a given transitions is not allowed, or an allowed transition is missing.



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
# File 'lib/state_gate/rspec/allow_transitions_on.rb', line 24

RSpec::Matchers.define :allow_transitions_on do |attr_name| # rubocop:disable Metrics/BlockLength
  ##
  # Expect the given attribute state to match all given transitions.
  #
  match do |source_obj| # :nodoc:
    # validate we have a state engine and parameters
    return false unless valid_setup?(attr_name, source_obj)

    allowed_transitions  = source_obj.stateables[@key]
                                     .transitions_for_state(@state)
    expected_transitions = @to.map(&:to_s).map(&:to_sym)
    @missing_states      = allowed_transitions - expected_transitions
    @extra_states        = expected_transitions - allowed_transitions

    @error = :missing_states if @missing_states.any?
    @error = :extra_states   if @extra_states.any?

    @error ? false : true
  end



  # Expect the attribute state not to have any given transitions.
  #
  match_when_negated do |source_obj|
    # validate we have a state engine and parameters
    return false unless valid_setup?(attr_name, source_obj)

    allowed_transitions  = source_obj.stateables[@key]
                                     .transitions_for_state(@state)
    expected_transitions = @to.map(&:to_s).map(&:to_sym)
    remaining_states     = expected_transitions - allowed_transitions

    unless remaining_states.count == expected_transitions.count
      @error             = :found_states
      @found_states      = expected_transitions - remaining_states
    end

    @error ? false : true
  end



  # The state to be checked.
  chain :from do |state|
    @state = state
  end



  # The transitions to check
  chain :to do |*transitions|
    @to        = transitions.flatten
    @to_called = true
  end



  # Failure messages for an expected match.
  #
  failure_message do
    case @error
    when :no_state_gates
      "no state machines are defined for #{@source_name}."

    when :invalid_key
      "no state machine is defined for ##{@key}."

    when :invalid_state
      ":#{@state} is not a valid state for #{@source_name}##{@key}."

    when :no_from
      'missing ".from(<state>)".'

    when :no_to
      'missing ".to(<states>)".'

    when :invalid_transition_states
      states = @invalid_states.map { |s| ":#{s}" }
      if states.one?
        "#{states.first} is not a valid ##{@key} state."
      else
        "#{states.to_sentence} are not valid ##{@key} states."
      end

    when :missing_states
      states = @missing_states.map { |s| ":#{s}" }
      "##{@key} also transitions from :#{@state} to #{states.to_sentence}."

    when :extra_states
      states = @extra_states.map { |s| ":#{s}" }
      "##{@key} does not transition from :#{@state} to #{states.to_sentence}."
    end
  end



  # failure messages for a negated match.
  #
  failure_message_when_negated do
    case @error
    when :no_state_gates
      "no state machines are defined for #{@source_name}."

    when :invalid_key
      "no state machine is defined for ##{@key}."

    when :invalid_state
      ":#{@state} is not a valid state for #{@source_name}##{@key}."

    when :no_from
      'missing ".from(<state>)".'

    when :no_to
      'missing ".to(<states>)".'

    when :invalid_transition_states
      states = @invalid_states.map { |s| ":#{s}" }
      if states.one?
        "#{states.first} is not a valid ##{@key} state."
      else
        "#{states.to_sentence} are not valid ##{@key} states."
      end

    when :found_states
      states = @found_states.map { |s| ":#{s}" }
      ":#{@state} is allowed to transition to #{states.to_sentence}."
    end
  end



  # = Helpers
  # ======================================================================

  # Check the setup is correct with the required information available.
  #
  def valid_setup?(attr_name, source_obj) # :nodoc:
    @key            = StateGate.symbolize(attr_name)
    @state          = StateGate.symbolize(@state)
    @source_name    = source_obj.is_a?(Class) ? source_obj.name : source_obj.class.name

    # detect_setup_errors(source_obj)

    return false unless assert_state_gate(source_obj)
    return false unless assert_valid_key(source_obj)
    return false unless assert_from_present
    return false unless assert_valid_state
    return false unless assert_to_present

    assert_valid_transition
  end



  # Validate the state machines container exists
  #
  def assert_state_gate(source_obj)
    return true if source_obj.respond_to?(:stateables)

    @error = :no_state_gates
    false
  end



  # Validate the state machine is there
  #
  def assert_valid_key(source_obj)
    @eng = source_obj.stateables[@key]
    return true unless @eng.blank?

    @error = :invalid_key
    false
  end



  # Validate the :from state is present
  #
  def assert_from_present
    return true unless @state.blank?

    @error = :no_from
    false
  end



  # Validate it is a valid state supplied
  #
  def assert_valid_state
    return true if @eng.states.include?(@state)

    @error = :invalid_state
    false
  end



  # Validate the transitions have been supplied
  #
  def assert_to_present
    return true if @to_called

    @error = :no_to
    false
  end



  # Validate the supplied transitions are valid
  #
  def assert_valid_transition
    return true unless invalid_transition_states?

    @error = :invalid_transition_states
    false
  end



  # Check the supplied transition states are valid for the attribute.
  #
  def invalid_transition_states? # :nodoc:
    @invalid_states = []
    @to.each do |state|
      unless @eng.states.include?(state.to_s.to_sym)
        @invalid_states << state.to_s.to_sym
        @error = :invalid_transition_states
      end
    end

    @invalid_states.any? ? true : false
  end
end

#have_statesObject

Description

RSpec matcher to verify defined states.

:source_obj

The Class or Instance to be tested.

:for

The attrbute being tested.

:states

The expected states as Symbols or Strings

expect(User).to have_states(:pending, :active).for(:status)

Fails if an exisiting state is missing or there are defined states that have not been included.



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/state_gate/rspec/have_states.rb', line 22

RSpec::Matchers.define :have_states do |*states| # rubocop:disable Metrics/BlockLength
  #
  # Expect the given states to match all the states for the attribute.
  #
  match do |source_obj| # :nodoc:
    # validate we have a state engine and parameters
    return false unless valid_setup?(states, source_obj)

    @missing_states = @eng.states - @states
    @extra_states   = @states     - @eng.states

    @error = :missing_states if @missing_states.any?
    @error = :extra_states   if @extra_states.any?

    @error ? false : true
  end


  # Expect the attribute not to have any given states.
  #
  match_when_negated do |source_obj|
    # validate we have a state engine and parameters
    return false unless valid_setup?(states, source_obj)

    @valid_states = @states.select { |s| @eng.states.include?(s) }
    @error        = :valid_states_found if @valid_states.any?

    @error ? false : true
  end


  # The attribute that should have the expected states.
  #
  chain :for do |attr_name|
    @key = StateGate.symbolize(attr_name)
  end



  # Failure messages for an expected match.
  #
  failure_message do
    case @error
    when :no_state_gates
      "no state machines are defined for #{@source_name}."

    when :missing_key
      'missing ".for(<attribute>)".'

    when :invalid_key
      "no state machine is defined for ##{@key}."

    when :missing_states
      states = @missing_states.map { |s| ":#{s}" }
      if states.one?
        "#{states.first} is also a valid state for ##{@key}."
      else
        "#{states.to_sentence} are also valid states for ##{@key}."
      end

    when :extra_states
      states = @extra_states.map { |s| ":#{s}" }
      if states.one?
        "#{states.first} is not a valid state for ##{@key}."
      else
        "#{states.to_sentence} are not valid states for ##{@key}."
      end
    end
  end



  # failure messages for a negated match.
  #
  failure_message_when_negated do
    case @error
    when :no_state_gates
      "no state machines are defined for #{@source_name}."

    when :missing_key
      'missing ".for(<attribute>)".'

    when :invalid_key
      "no state machine is defined for ##{@key}."

    when :valid_states_found
      states = @valid_states.map { |s| ":#{s}" }
      if states.one?
        "#{states.first} is a valid state for ##{@key}."
      else
        "#{states.to_sentence} are valid states for ##{@key}."
      end
    end
  end



  #  Helpers
  # ======================================================================

  # Check the setup is correct with the required information available.
  #
  def valid_setup?(states, source_obj) # :nodoc:
    @states         = states.flatten.map { |s| StateGate.symbolize(s) }
    @source_name    = source_obj.is_a?(Class) ? source_obj.name : source_obj.class.name

    if @key.blank?
      @error = :missing_key

    elsif !source_obj.respond_to?(:stateables)
      @error = :no_state_gates

    elsif (@eng = source_obj.stateables[@key]).blank?
      @error = :invalid_key
    end

    @error ? false : true
  end
end