File: modern_callback_api_test.rb

package info (click to toggle)
ruby-state-machines 0.100.4-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 2,600 kB
  • sloc: ruby: 18,854; sh: 17; makefile: 4
file content (266 lines) | stat: -rw-r--r-- 7,915 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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
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
260
261
262
263
264
265
266
# frozen_string_literal: true

require 'test_helper'

class ModernCallbackApiTest < StateMachinesTest
  def setup
    @model = Class.new do
      attr_accessor :submitted_at, :approved_at, :rejected_at, :published_at, :callback_log

      def initialize
        @callback_log = []
        super
      end

      state_machine :status, initial: :draft do
        event :submit do
          transition draft: :pending
        end

        event :approve do
          transition pending: :approved
        end

        event :reject do
          transition pending: :rejected
        end

        event :publish do
          transition approved: :published
        end

        # Modern keyword argument style - cleaner and more explicit
        before_transition(from: :draft, to: :pending, do: :validate_submission)
        before_transition(from: :pending, to: :approved, if: :meets_criteria?, do: :validate_approval)

        # Pattern matching in callback blocks showcases Ruby 3.2+ features
        before_transition do |object, transition|
          case [transition.from_name, transition.to_name, transition.event]
          when %i[draft pending submit]
            object.submitted_at = Time.now
            object.callback_log << 'submitting for review'
          when %i[pending approved approve]
            object.approved_at = Time.now
            object.callback_log << 'approving submission'
          when %i[pending rejected reject]
            object.rejected_at = Time.now
            object.callback_log << 'rejecting submission'
          when %i[approved published publish]
            object.published_at = Time.now
            object.callback_log << 'publishing content'
          else
            object.callback_log << "transition: #{transition.from} -> #{transition.to} via #{transition.event}"
          end
        end

        # Mixed style: legacy positional args with modern pattern matching
        after_transition :notify_stakeholders do |object, transition|
          # Modern pattern matching for notification logic
          object.callback_log << case transition.to_name
                                 when :published
                                   'sending publication notifications'
                                 when :rejected
                                   'sending rejection notifications'
                                 else
                                   "status changed to #{transition.to_name}"
                                 end
        end

        # Modern keyword style with multiple conditions
        around_transition(from: :pending, on: %i[approve reject]) do |object, _transition, block|
          object.callback_log << 'starting review process'
          start_time = Time.now

          block.call # Execute the transition

          duration = Time.now - start_time
          object.callback_log << "review completed in #{duration.round(2)} seconds"
        end
      end

      private

      def validate_submission
        callback_log << 'validating submission'
      end

      def meets_criteria?
        callback_log << 'checking approval criteria'
        true # Simplified for test
      end

      def validate_approval
        callback_log << 'validating approval'
      end

      def notify_stakeholders
        callback_log << 'notifying stakeholders'
      end
    end

    @workflow = @model.new
  end

  def test_should_support_modern_keyword_arguments
    assert_equal 'draft', @workflow.status
    assert_empty @workflow.callback_log

    # Test submit transition with modern callbacks
    @workflow.submit

    assert_equal 'pending', @workflow.status
    assert_includes @workflow.callback_log, 'validating submission'
    assert_includes @workflow.callback_log, 'submitting for review'
    assert_includes @workflow.callback_log, 'notifying stakeholders'
    assert_includes @workflow.callback_log, 'status changed to pending'
    refute_nil @workflow.submitted_at
  end

  def test_should_support_pattern_matching_in_callbacks
    @workflow.submit
    @workflow.approve

    assert_equal 'approved', @workflow.status
    assert_includes @workflow.callback_log, 'checking approval criteria'
    assert_includes @workflow.callback_log, 'validating approval'
    assert_includes @workflow.callback_log, 'approving submission'
    assert_includes @workflow.callback_log, 'starting review process'
    assert(@workflow.callback_log.any? { |log| log.start_with?('review completed in') })
    refute_nil @workflow.approved_at
  end

  def test_should_support_mixed_callback_styles
    @workflow.submit
    @workflow.approve
    @workflow.publish

    assert_equal 'published', @workflow.status
    assert_includes @workflow.callback_log, 'publishing content'
    assert_includes @workflow.callback_log, 'sending publication notifications'
    refute_nil @workflow.published_at
  end

  def test_should_maintain_backward_compatibility_with_legacy_callbacks
    model = Class.new do
      attr_accessor :callback_log

      def initialize
        @callback_log = []
        super
      end

      state_machine :status, initial: :draft do
        event :submit do
          transition draft: :pending
        end

        # All legacy callback styles should still work
        before_transition :log_before_any
        before_transition draft: :pending, do: :log_submit
        before_transition on: :submit, do: [:log_event_submit]
        before_transition from: :draft, to: :pending, if: :should_log?, do: :log_conditional
      end

      private

      def log_before_any
        callback_log << 'before any'
      end

      def log_submit
        callback_log << 'submit transition'
      end

      def log_event_submit
        callback_log << 'submit event'
      end

      def log_conditional
        callback_log << 'conditional callback'
      end

      def should_log?
        true
      end
    end

    workflow = model.new
    workflow.submit

    assert_equal 'pending', workflow.status
    assert_includes workflow.callback_log, 'before any'
    assert_includes workflow.callback_log, 'submit transition'
    assert_includes workflow.callback_log, 'submit event'
    assert_includes workflow.callback_log, 'conditional callback'
  end

  def test_should_support_block_only_callbacks
    model = Class.new do
      attr_accessor :callback_log

      def initialize
        @callback_log = []
        super
      end

      state_machine :status, initial: :draft do
        event :submit do
          transition draft: :pending
        end

        # Block-only callback should work
        before_transition do |object|
          object.callback_log << 'block only callback'
        end
      end
    end

    workflow = model.new
    workflow.submit

    assert_equal 'pending', workflow.status
    assert_includes workflow.callback_log, 'block only callback'
  end

  def test_should_support_pure_keyword_style_without_positional_args
    model = Class.new do
      attr_accessor :callback_log

      def initialize
        @callback_log = []
        super
      end

      state_machine :status, initial: :draft do
        event :submit do
          transition draft: :pending
        end

        # Pure keyword arguments without any positional args
        before_transition(from: :draft, to: :pending, do: :log_pure_keyword)
        before_transition(on: :submit, if: :should_log?, do: :log_event_keyword)
      end

      private

      def log_pure_keyword
        callback_log << 'pure keyword callback'
      end

      def log_event_keyword
        callback_log << 'event keyword callback'
      end

      def should_log?
        true
      end
    end

    workflow = model.new
    workflow.submit

    assert_equal 'pending', workflow.status
    assert_includes workflow.callback_log, 'pure keyword callback'
    assert_includes workflow.callback_log, 'event keyword callback'
  end
end