stepper_motor 0.1.16 → 0.1.18
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 +4 -4
- data/CHANGELOG.md +9 -0
- data/lib/stepper_motor/conditional.rb +42 -0
- data/lib/stepper_motor/journey.rb +48 -13
- data/lib/stepper_motor/step.rb +2 -14
- data/lib/stepper_motor/version.rb +1 -1
- data/lib/stepper_motor.rb +1 -0
- data/manual/MANUAL.md +117 -0
- data/rbi/stepper_motor.rbi +36 -1
- data/sig/stepper_motor.rbs +30 -0
- data/stepper_motor.gemspec +1 -1
- data/test/stepper_motor/journey/cancel_if_test.rb +293 -0
- data/test/stepper_motor/wrap_conditional_test.rb +190 -0
- data/test/test_helper.rb +3 -2
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2b4b58accbed9841696bb2d0296335863b71b26b9a33a457028cc4c834ec1fec
|
4
|
+
data.tar.gz: c0f6e6fda44e9d8c7808743e425a059e2a8c1196fc4fcd1fcb6240521d8b5bbf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2b3aa12d9c21cf9f5e8246bce6fa8d88bfaeea046ad434ada641c7c0e1ce62cba173584fd4c836c6e70d98468f1ebc70ce93ea0246fb40ed3992ea45a97328f2
|
7
|
+
data.tar.gz: ae3fb23fc0f81f3a92e63abf6a6b444a801421c388ed3ef3a7e6ff23562a74bca218d3b257cb7a6dfa896e383d4e093bd990f67209f4ca41936598db91e680a9
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,15 @@
|
|
2
2
|
|
3
3
|
## [Unreleased]
|
4
4
|
|
5
|
+
## [0.1.18] - 2025-06-20
|
6
|
+
|
7
|
+
- Add `cancel_if` at Journey class level for blanket journey cancellation conditions. This makes it very easy to abort journeys across multiple steps.
|
8
|
+
|
9
|
+
## [0.1.17] - 2025-06-20
|
10
|
+
|
11
|
+
- Mandate Ruby 3.1+ because we are using 3.x syntax and there are shenanigans with sqlite3 gem version and locking. Compatibility with 2.7 can be assured but is too much hassle at the moment.
|
12
|
+
If you need this compatibility, feel free to implement it or contact me for a quote.
|
13
|
+
|
5
14
|
## [0.1.16] - 2025-06-20
|
6
15
|
|
7
16
|
- Add `skip!` flow control method to skip the current (or next) step and move on to the subsequent step, or finish the journey.
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module StepperMotor
|
4
|
+
# A wrapper for conditional logic that can be evaluated against an object.
|
5
|
+
# This class encapsulates different types of conditions (booleans, symbols, callables, arrays)
|
6
|
+
# and provides a unified interface for checking if a condition is satisfied by a given object.
|
7
|
+
# It handles negation and ensures proper context when evaluating conditions.
|
8
|
+
class Conditional
|
9
|
+
def initialize(condition, negate: false)
|
10
|
+
@condition = condition
|
11
|
+
@negate = negate
|
12
|
+
validate_condition
|
13
|
+
end
|
14
|
+
|
15
|
+
def satisfied_by?(object)
|
16
|
+
result = case @condition
|
17
|
+
when Array
|
18
|
+
@condition.all? { |c| Conditional.new(c).satisfied_by?(object) }
|
19
|
+
when Symbol
|
20
|
+
!!object.send(@condition)
|
21
|
+
when Conditional
|
22
|
+
@condition.satisfied_by?(object)
|
23
|
+
else
|
24
|
+
if @condition.respond_to?(:call)
|
25
|
+
!!object.instance_exec(&@condition)
|
26
|
+
else
|
27
|
+
!!@condition
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
@negate ? !result : result
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def validate_condition
|
37
|
+
unless [true, false, nil].include?(@condition) || @condition.is_a?(Symbol) || @condition.is_a?(Array) || @condition.is_a?(Conditional) || @condition.respond_to?(:call)
|
38
|
+
raise ArgumentError, "condition must be a boolean, nil, Symbol, Array, Conditional or a callable object, but was a #{@condition.inspect}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -47,6 +47,9 @@ module StepperMotor
|
|
47
47
|
# @return [Array<StepperMotor::Step>] the step definitions defined so far
|
48
48
|
class_attribute :step_definitions, default: []
|
49
49
|
|
50
|
+
# @return [Array<StepperMotor::Conditional>] the cancel_if conditions defined for this journey class
|
51
|
+
class_attribute :cancel_if_conditions, default: []
|
52
|
+
|
50
53
|
belongs_to :hero, polymorphic: true, optional: true
|
51
54
|
|
52
55
|
STATES = %w[ready paused performing canceled finished]
|
@@ -92,19 +95,8 @@ module StepperMotor
|
|
92
95
|
raise StepConfigurationError, "Either skip_if: or if: can be specified, but not both"
|
93
96
|
end
|
94
97
|
if additional_step_definition_options.key?(:if)
|
95
|
-
|
96
|
-
|
97
|
-
# if: truthy means perform, skip_if: truthy means "skip"
|
98
|
-
additional_step_definition_options[:skip_if] = case if_condition
|
99
|
-
when true, false, nil
|
100
|
-
!if_condition
|
101
|
-
when Symbol
|
102
|
-
# For symbols, we need to create a proc that negates the result
|
103
|
-
-> { !send(if_condition) }
|
104
|
-
else
|
105
|
-
# For callables, we need to create a proc that negates the result
|
106
|
-
-> { !instance_exec(&if_condition) }
|
107
|
-
end
|
98
|
+
# Convert if: to skip_if:
|
99
|
+
additional_step_definition_options[:skip_if] = StepperMotor::Conditional.new(additional_step_definition_options.delete(:if), negate: true)
|
108
100
|
end
|
109
101
|
|
110
102
|
wait = if wait && after
|
@@ -166,6 +158,41 @@ module StepperMotor
|
|
166
158
|
self.class.lookup_step_definition(by_step_name)
|
167
159
|
end
|
168
160
|
|
161
|
+
# Defines a condition that will cause the journey to cancel if satisfied.
|
162
|
+
# This works like Rails' `etag` - it's class-inheritable and appendable.
|
163
|
+
# Multiple `cancel_if` calls can be made to a Journey definition.
|
164
|
+
# All conditions are evaluated after setting the state to `performing`.
|
165
|
+
# If any condition is satisfied, the journey will cancel.
|
166
|
+
#
|
167
|
+
# @param condition_arg [TrueClass, FalseClass, Symbol, Proc, Array, Conditional] the condition to check
|
168
|
+
# @param condition_blk [Proc] a block that will be evaluated as a condition
|
169
|
+
# @return [void]
|
170
|
+
def self.cancel_if(condition_arg = :__no_argument_given__, &condition_blk)
|
171
|
+
# Check if neither argument nor block is provided
|
172
|
+
if condition_arg == :__no_argument_given__ && !condition_blk
|
173
|
+
raise ArgumentError, "cancel_if requires either a condition argument or a block"
|
174
|
+
end
|
175
|
+
|
176
|
+
# Check if both argument and block are provided
|
177
|
+
if condition_arg != :__no_argument_given__ && condition_blk
|
178
|
+
raise ArgumentError, "cancel_if accepts either a condition argument or a block, but not both"
|
179
|
+
end
|
180
|
+
|
181
|
+
# Select the condition: positional argument takes precedence if not sentinel
|
182
|
+
condition = if condition_arg != :__no_argument_given__
|
183
|
+
condition_arg
|
184
|
+
else
|
185
|
+
condition_blk
|
186
|
+
end
|
187
|
+
|
188
|
+
conditional = StepperMotor::Conditional.new(condition)
|
189
|
+
|
190
|
+
# As per Rails docs: you need to be aware when using class_attribute with mutable structures
|
191
|
+
# as Array or Hash. In such cases, you don't want to do changes in place. Instead use setters.
|
192
|
+
# See https://apidock.com/rails/v7.1.3.2/Class/class_attribute
|
193
|
+
self.cancel_if_conditions = cancel_if_conditions + [conditional]
|
194
|
+
end
|
195
|
+
|
169
196
|
# Performs the next step in the journey. Will check whether any other process has performed the step already
|
170
197
|
# and whether the record is unchanged, and will then lock it and set the state to 'performimg'.
|
171
198
|
#
|
@@ -188,6 +215,14 @@ module StepperMotor
|
|
188
215
|
performing!
|
189
216
|
after_locking_for_step(next_step_name)
|
190
217
|
end
|
218
|
+
|
219
|
+
# Check cancel_if conditions after setting state to performing
|
220
|
+
if cancel_if_conditions.any? { |conditional| conditional.satisfied_by?(self) }
|
221
|
+
logger.info { "cancel_if condition satisfied, canceling journey" }
|
222
|
+
cancel!
|
223
|
+
return
|
224
|
+
end
|
225
|
+
|
191
226
|
current_step_name = next_step_name
|
192
227
|
|
193
228
|
if current_step_name
|
data/lib/stepper_motor/step.rb
CHANGED
@@ -36,12 +36,7 @@ class StepperMotor::Step
|
|
36
36
|
@wait = wait
|
37
37
|
@seq = seq
|
38
38
|
@on_exception = on_exception # TODO: Validate?
|
39
|
-
@skip_if_condition = skip_if
|
40
|
-
|
41
|
-
# Validate the skip_if condition
|
42
|
-
if ![true, false, nil].include?(@skip_if_condition) && !@skip_if_condition.is_a?(Symbol) && !@skip_if_condition.respond_to?(:call)
|
43
|
-
raise ArgumentError, "skip_if: condition must be a boolean, nil, Symbol or a callable object, but was a #{@skip_if_condition.inspect}"
|
44
|
-
end
|
39
|
+
@skip_if_condition = StepperMotor::Conditional.new(skip_if)
|
45
40
|
end
|
46
41
|
|
47
42
|
# Checks if the step should be skipped based on the skip_if condition
|
@@ -49,14 +44,7 @@ class StepperMotor::Step
|
|
49
44
|
# @param journey[StepperMotor::Journey] the journey to check the condition for
|
50
45
|
# @return [Boolean] true if the step should be skipped, false otherwise
|
51
46
|
def should_skip?(journey)
|
52
|
-
|
53
|
-
when true, false, nil
|
54
|
-
!!@skip_if_condition
|
55
|
-
when Symbol
|
56
|
-
journey.send(@skip_if_condition) # Allow private methods
|
57
|
-
else
|
58
|
-
journey.instance_exec(&@skip_if_condition)
|
59
|
-
end
|
47
|
+
@skip_if_condition.satisfied_by?(journey)
|
60
48
|
end
|
61
49
|
|
62
50
|
# Performs the step on the passed Journey, wrapping the step with the required context.
|
data/lib/stepper_motor.rb
CHANGED
@@ -13,6 +13,7 @@ module StepperMotor
|
|
13
13
|
|
14
14
|
autoload :Journey, File.dirname(__FILE__) + "/stepper_motor/journey.rb"
|
15
15
|
autoload :Step, File.dirname(__FILE__) + "/stepper_motor/step.rb"
|
16
|
+
autoload :Conditional, File.dirname(__FILE__) + "/stepper_motor/conditional.rb"
|
16
17
|
|
17
18
|
autoload :BaseJob, File.dirname(__FILE__) + "/stepper_motor/base_job.rb"
|
18
19
|
autoload :PerformStepJob, File.dirname(__FILE__) + "/stepper_motor/perform_step_job.rb"
|
data/manual/MANUAL.md
CHANGED
@@ -515,6 +515,123 @@ It is possible to store instance variables on the `Journey` instance, but they d
|
|
515
515
|
> This means that the volatile state such as instance variables is not going to be available between steps. Always assume that
|
516
516
|
> the `Journey` you are inside of does not have any instance variables set by previous steps and has just been freshly loaded from the database.
|
517
517
|
|
518
|
+
### Blanket step conditions with `cancel_if`
|
519
|
+
|
520
|
+
In addition to conditional steps that can be skipped, you can define blanket conditions that apply to all steps in a journey using `cancel_if`. These conditions are evaluated after the journey's state is set to `performing` but before any step execution begins. If any `cancel_if` condition is satisfied, the entire journey is canceled immediately.
|
521
|
+
|
522
|
+
`cancel_if` works like Rails' `etag` - it's class-inheritable and appendable. You can call `cancel_if` multiple times in a journey definition, and all conditions will be evaluated.
|
523
|
+
|
524
|
+
#### Using blocks with `cancel_if`
|
525
|
+
|
526
|
+
The most common way to use `cancel_if` is with a block that gets evaluated in the context of the journey:
|
527
|
+
|
528
|
+
```ruby
|
529
|
+
class UserOnboardingJourney < StepperMotor::Journey
|
530
|
+
cancel_if { hero.deactivated? }
|
531
|
+
cancel_if { hero.account_closed? }
|
532
|
+
|
533
|
+
step :send_welcome_email do
|
534
|
+
WelcomeMailer.welcome(hero).deliver_later
|
535
|
+
end
|
536
|
+
|
537
|
+
step :send_premium_offer, wait: 2.days do
|
538
|
+
PremiumOfferMailer.exclusive_offer(hero).deliver_later
|
539
|
+
end
|
540
|
+
|
541
|
+
step :complete_onboarding, wait: 7.days do
|
542
|
+
hero.update!(onboarding_completed_at: Time.current)
|
543
|
+
end
|
544
|
+
end
|
545
|
+
```
|
546
|
+
|
547
|
+
In this example, if the user becomes deactivated or closes their account at any point during the onboarding journey, the entire journey will be canceled and no further steps will be executed.
|
548
|
+
|
549
|
+
#### Using positional arguments with `cancel_if`
|
550
|
+
|
551
|
+
You can also pass conditions directly as arguments to `cancel_if`:
|
552
|
+
|
553
|
+
```ruby
|
554
|
+
class PaymentProcessingJourney < StepperMotor::Journey
|
555
|
+
cancel_if :payment_canceled?
|
556
|
+
cancel_if { hero.amount > 10000 } # Cancel if amount exceeds limit
|
557
|
+
|
558
|
+
step :validate_payment do
|
559
|
+
PaymentValidator.validate(hero)
|
560
|
+
end
|
561
|
+
|
562
|
+
step :process_payment do
|
563
|
+
PaymentProcessor.charge(hero)
|
564
|
+
end
|
565
|
+
|
566
|
+
step :send_confirmation do
|
567
|
+
PaymentConfirmationMailer.confirm(hero).deliver_later
|
568
|
+
end
|
569
|
+
|
570
|
+
private
|
571
|
+
|
572
|
+
def payment_canceled?
|
573
|
+
hero.canceled_at.present?
|
574
|
+
end
|
575
|
+
end
|
576
|
+
```
|
577
|
+
|
578
|
+
#### Supported condition types
|
579
|
+
|
580
|
+
`cancel_if` accepts the same types of conditions as `skip_if`:
|
581
|
+
|
582
|
+
* **Symbols**: Method names that return a boolean
|
583
|
+
* **Blocks**: Callable blocks that return a boolean
|
584
|
+
* **Booleans**: Literal `true` or `false` values
|
585
|
+
* **Arrays**: Arrays of conditions (all must be true)
|
586
|
+
* **Procs/Lambdas**: Callable objects
|
587
|
+
* **Conditional objects**: `StepperMotor::Conditional` instances
|
588
|
+
* **Nil**: Treated as `false` (doesn't cancel)
|
589
|
+
|
590
|
+
#### Multiple `cancel_if` conditions
|
591
|
+
|
592
|
+
You can define multiple `cancel_if` conditions, and any one of them being satisfied will cancel the journey:
|
593
|
+
|
594
|
+
```ruby
|
595
|
+
class EmailCampaignJourney < StepperMotor::Journey
|
596
|
+
cancel_if { hero.unsubscribed? }
|
597
|
+
cancel_if { hero.bounced? }
|
598
|
+
cancel_if { hero.complained? }
|
599
|
+
cancel_if { hero.deactivated? }
|
600
|
+
|
601
|
+
step :send_initial_email do
|
602
|
+
CampaignMailer.initial(hero).deliver_later
|
603
|
+
end
|
604
|
+
|
605
|
+
step :send_follow_up, wait: 3.days do
|
606
|
+
CampaignMailer.follow_up(hero).deliver_later
|
607
|
+
end
|
608
|
+
|
609
|
+
step :send_final_reminder, wait: 7.days do
|
610
|
+
CampaignMailer.final_reminder(hero).deliver_later
|
611
|
+
end
|
612
|
+
end
|
613
|
+
```
|
614
|
+
|
615
|
+
#### Inheritance and appendability
|
616
|
+
|
617
|
+
`cancel_if` conditions are class-inheritable and appendable, just like Rails' `etag`:
|
618
|
+
|
619
|
+
```ruby
|
620
|
+
class BaseJourney < StepperMotor::Journey
|
621
|
+
cancel_if { hero.deactivated? }
|
622
|
+
end
|
623
|
+
|
624
|
+
class PremiumUserJourney < BaseJourney
|
625
|
+
cancel_if { hero.subscription_expired? } # Adds to parent's conditions
|
626
|
+
|
627
|
+
step :send_premium_content do
|
628
|
+
PremiumContentMailer.send_content(hero).deliver_later
|
629
|
+
end
|
630
|
+
end
|
631
|
+
```
|
632
|
+
|
633
|
+
In this example, `PremiumUserJourney` will cancel if either the user is deactivated (from the parent class) or if their subscription has expired (from the child class).
|
634
|
+
|
518
635
|
|
519
636
|
### Waiting for the start of the step
|
520
637
|
|
data/rbi/stepper_motor.rbi
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
# StepperMotor is a module for building multi-step flows where steps are sequential and only
|
3
3
|
# ever progress forward. The building block of StepperMotor is StepperMotor::Journey
|
4
4
|
module StepperMotor
|
5
|
-
VERSION = T.let("0.1.
|
5
|
+
VERSION = T.let("0.1.18", T.untyped)
|
6
6
|
PerformStepJobV2 = T.let(StepperMotor::PerformStepJob, T.untyped)
|
7
7
|
RecoverStuckJourneysJobV1 = T.let(StepperMotor::RecoverStuckJourneysJob, T.untyped)
|
8
8
|
|
@@ -124,6 +124,10 @@ module StepperMotor
|
|
124
124
|
sig { returns(T.untyped) }
|
125
125
|
def step_definitions; end
|
126
126
|
|
127
|
+
# _@return_ — the cancel_if conditions defined for this journey class
|
128
|
+
sig { returns(T::Array[StepperMotor::Conditional]) }
|
129
|
+
def cancel_if_conditions; end
|
130
|
+
|
127
131
|
# sord duck - #to_f looks like a duck type, replacing with untyped
|
128
132
|
# sord warn - ActiveSupport::Duration wasn't able to be resolved to a constant in this project
|
129
133
|
# sord duck - #to_f looks like a duck type, replacing with untyped
|
@@ -173,6 +177,18 @@ module StepperMotor
|
|
173
177
|
sig { params(by_step_name: T.untyped).returns(T.untyped) }
|
174
178
|
def lookup_step_definition(by_step_name); end
|
175
179
|
|
180
|
+
# Defines a condition that will cause the journey to cancel if satisfied.
|
181
|
+
# This works like Rails' `etag` - it's class-inheritable and appendable.
|
182
|
+
# Multiple `cancel_if` calls can be made to a Journey definition.
|
183
|
+
# All conditions are evaluated after setting the state to `performing`.
|
184
|
+
# If any condition is satisfied, the journey will cancel.
|
185
|
+
#
|
186
|
+
# _@param_ `condition_arg` — the condition to check
|
187
|
+
#
|
188
|
+
# _@param_ `condition_blk` — a block that will be evaluated as a condition
|
189
|
+
sig { params(condition_arg: T.any(TrueClass, FalseClass, Symbol, Proc, T::Array[T.untyped], Conditional), condition_blk: T.untyped).void }
|
190
|
+
def self.cancel_if(condition_arg = :__no_argument_given__, &condition_blk); end
|
191
|
+
|
176
192
|
# Performs the next step in the journey. Will check whether any other process has performed the step already
|
177
193
|
# and whether the record is unchanged, and will then lock it and set the state to 'performimg'.
|
178
194
|
#
|
@@ -359,6 +375,25 @@ module StepperMotor
|
|
359
375
|
class BaseJob < ActiveJob::Base
|
360
376
|
end
|
361
377
|
|
378
|
+
# A wrapper for conditional logic that can be evaluated against an object.
|
379
|
+
# This class encapsulates different types of conditions (booleans, symbols, callables, arrays)
|
380
|
+
# and provides a unified interface for checking if a condition is satisfied by a given object.
|
381
|
+
# It handles negation and ensures proper context when evaluating conditions.
|
382
|
+
class Conditional
|
383
|
+
# sord omit - no YARD type given for "condition", using untyped
|
384
|
+
# sord omit - no YARD type given for "negate:", using untyped
|
385
|
+
sig { params(condition: T.untyped, negate: T.untyped).void }
|
386
|
+
def initialize(condition, negate: false); end
|
387
|
+
|
388
|
+
# sord omit - no YARD type given for "object", using untyped
|
389
|
+
sig { params(object: T.untyped).returns(T::Boolean) }
|
390
|
+
def satisfied_by?(object); end
|
391
|
+
|
392
|
+
# sord omit - no YARD return type given, using untyped
|
393
|
+
sig { returns(T.untyped) }
|
394
|
+
def validate_condition; end
|
395
|
+
end
|
396
|
+
|
362
397
|
module TestHelper
|
363
398
|
# Allows running a given Journey to completion, skipping across the waiting periods.
|
364
399
|
# This is useful to evaluate all side effects of a Journey. The helper will ensure
|
data/sig/stepper_motor.rbs
CHANGED
@@ -112,6 +112,9 @@ module StepperMotor
|
|
112
112
|
# _@see_ `Journey.step_definitions`
|
113
113
|
def step_definitions: () -> untyped
|
114
114
|
|
115
|
+
# _@return_ — the cancel_if conditions defined for this journey class
|
116
|
+
def cancel_if_conditions: () -> ::Array[StepperMotor::Conditional]
|
117
|
+
|
115
118
|
# sord duck - #to_f looks like a duck type, replacing with untyped
|
116
119
|
# sord warn - ActiveSupport::Duration wasn't able to be resolved to a constant in this project
|
117
120
|
# sord duck - #to_f looks like a duck type, replacing with untyped
|
@@ -155,6 +158,17 @@ module StepperMotor
|
|
155
158
|
# _@see_ `Journey.lookup_step_definition`
|
156
159
|
def lookup_step_definition: (untyped by_step_name) -> untyped
|
157
160
|
|
161
|
+
# Defines a condition that will cause the journey to cancel if satisfied.
|
162
|
+
# This works like Rails' `etag` - it's class-inheritable and appendable.
|
163
|
+
# Multiple `cancel_if` calls can be made to a Journey definition.
|
164
|
+
# All conditions are evaluated after setting the state to `performing`.
|
165
|
+
# If any condition is satisfied, the journey will cancel.
|
166
|
+
#
|
167
|
+
# _@param_ `condition_arg` — the condition to check
|
168
|
+
#
|
169
|
+
# _@param_ `condition_blk` — a block that will be evaluated as a condition
|
170
|
+
def self.cancel_if: (?(TrueClass | FalseClass | Symbol | Proc | ::Array[untyped] | Conditional) condition_arg) -> void
|
171
|
+
|
158
172
|
# Performs the next step in the journey. Will check whether any other process has performed the step already
|
159
173
|
# and whether the record is unchanged, and will then lock it and set the state to 'performimg'.
|
160
174
|
#
|
@@ -320,6 +334,22 @@ module StepperMotor
|
|
320
334
|
class BaseJob < ActiveJob::Base
|
321
335
|
end
|
322
336
|
|
337
|
+
# A wrapper for conditional logic that can be evaluated against an object.
|
338
|
+
# This class encapsulates different types of conditions (booleans, symbols, callables, arrays)
|
339
|
+
# and provides a unified interface for checking if a condition is satisfied by a given object.
|
340
|
+
# It handles negation and ensures proper context when evaluating conditions.
|
341
|
+
class Conditional
|
342
|
+
# sord omit - no YARD type given for "condition", using untyped
|
343
|
+
# sord omit - no YARD type given for "negate:", using untyped
|
344
|
+
def initialize: (untyped condition, ?negate: untyped) -> void
|
345
|
+
|
346
|
+
# sord omit - no YARD type given for "object", using untyped
|
347
|
+
def satisfied_by?: (untyped object) -> bool
|
348
|
+
|
349
|
+
# sord omit - no YARD return type given, using untyped
|
350
|
+
def validate_condition: () -> untyped
|
351
|
+
end
|
352
|
+
|
323
353
|
module TestHelper
|
324
354
|
# Allows running a given Journey to completion, skipping across the waiting periods.
|
325
355
|
# This is useful to evaluate all side effects of a Journey. The helper will ensure
|
data/stepper_motor.gemspec
CHANGED
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
|
|
12
12
|
spec.summary = "Effortless step workflows that embed nicely inside Rails"
|
13
13
|
spec.description = "Step workflows for Rails/ActiveRecord"
|
14
14
|
spec.homepage = "https://steppermotor.dev"
|
15
|
-
spec.required_ruby_version = ">=
|
15
|
+
spec.required_ruby_version = ">= 3.1.0"
|
16
16
|
|
17
17
|
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
18
18
|
|
@@ -0,0 +1,293 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_helper"
|
4
|
+
|
5
|
+
class CancelIfTest < ActiveSupport::TestCase
|
6
|
+
include ActiveJob::TestHelper
|
7
|
+
include SideEffects::TestHelper
|
8
|
+
|
9
|
+
test "cancel_if with block condition cancels journey when true" do
|
10
|
+
canceling_journey = create_journey_subclass do
|
11
|
+
cancel_if { true }
|
12
|
+
|
13
|
+
step do
|
14
|
+
SideEffects.touch! "should not run"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
journey = canceling_journey.create!
|
19
|
+
assert journey.ready?
|
20
|
+
|
21
|
+
assert_no_side_effects { journey.perform_next_step! }
|
22
|
+
assert journey.canceled?
|
23
|
+
end
|
24
|
+
|
25
|
+
test "cancel_if with block condition does not cancel journey when false" do
|
26
|
+
canceling_journey = create_journey_subclass do
|
27
|
+
cancel_if { false }
|
28
|
+
|
29
|
+
step do
|
30
|
+
SideEffects.touch! "should run"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
journey = canceling_journey.create!
|
35
|
+
assert journey.ready?
|
36
|
+
|
37
|
+
assert_produced_side_effects("should run") do
|
38
|
+
journey.perform_next_step!
|
39
|
+
end
|
40
|
+
assert journey.finished?
|
41
|
+
end
|
42
|
+
|
43
|
+
test "cancel_if with boolean argument cancels journey when true" do
|
44
|
+
canceling_journey = create_journey_subclass do
|
45
|
+
cancel_if true
|
46
|
+
|
47
|
+
step do
|
48
|
+
SideEffects.touch! "should not run"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
journey = canceling_journey.create!
|
53
|
+
assert journey.ready?
|
54
|
+
|
55
|
+
assert_no_side_effects { journey.perform_next_step! }
|
56
|
+
assert journey.canceled?
|
57
|
+
end
|
58
|
+
|
59
|
+
test "cancel_if with boolean argument does not cancel journey when false" do
|
60
|
+
canceling_journey = create_journey_subclass do
|
61
|
+
cancel_if false
|
62
|
+
|
63
|
+
step do
|
64
|
+
SideEffects.touch! "should run"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
journey = canceling_journey.create!
|
69
|
+
assert journey.ready?
|
70
|
+
|
71
|
+
assert_produced_side_effects("should run") do
|
72
|
+
journey.perform_next_step!
|
73
|
+
end
|
74
|
+
assert journey.finished?
|
75
|
+
end
|
76
|
+
|
77
|
+
test "cancel_if with symbol argument calls method on journey" do
|
78
|
+
canceling_journey = create_journey_subclass do
|
79
|
+
cancel_if :should_cancel?
|
80
|
+
|
81
|
+
step do
|
82
|
+
SideEffects.touch! "should not run"
|
83
|
+
end
|
84
|
+
|
85
|
+
def should_cancel?
|
86
|
+
true
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
journey = canceling_journey.create!
|
91
|
+
assert journey.ready?
|
92
|
+
|
93
|
+
assert_no_side_effects { journey.perform_next_step! }
|
94
|
+
assert journey.canceled?
|
95
|
+
end
|
96
|
+
|
97
|
+
test "cancel_if with symbol method call in block" do
|
98
|
+
canceling_journey = create_journey_subclass do
|
99
|
+
cancel_if { should_cancel? }
|
100
|
+
|
101
|
+
step do
|
102
|
+
SideEffects.touch! "should not run"
|
103
|
+
end
|
104
|
+
|
105
|
+
def should_cancel?
|
106
|
+
true
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
journey = canceling_journey.create!
|
111
|
+
assert journey.ready?
|
112
|
+
|
113
|
+
assert_no_side_effects { journey.perform_next_step! }
|
114
|
+
assert journey.canceled?
|
115
|
+
end
|
116
|
+
|
117
|
+
test "cancel_if with array argument cancels when all conditions are true" do
|
118
|
+
canceling_journey = create_journey_subclass do
|
119
|
+
cancel_if [true, true]
|
120
|
+
|
121
|
+
step do
|
122
|
+
SideEffects.touch! "should not run"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
journey = canceling_journey.create!
|
127
|
+
assert journey.ready?
|
128
|
+
|
129
|
+
assert_no_side_effects { journey.perform_next_step! }
|
130
|
+
assert journey.canceled?
|
131
|
+
end
|
132
|
+
|
133
|
+
test "cancel_if with array argument does not cancel when any condition is false" do
|
134
|
+
canceling_journey = create_journey_subclass do
|
135
|
+
cancel_if [true, false]
|
136
|
+
|
137
|
+
step do
|
138
|
+
SideEffects.touch! "should run"
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
journey = canceling_journey.create!
|
143
|
+
assert journey.ready?
|
144
|
+
|
145
|
+
assert_produced_side_effects("should run") do
|
146
|
+
journey.perform_next_step!
|
147
|
+
end
|
148
|
+
assert journey.finished?
|
149
|
+
end
|
150
|
+
|
151
|
+
test "cancel_if with proc argument evaluates proc in journey context" do
|
152
|
+
proc_condition = -> { true }
|
153
|
+
|
154
|
+
canceling_journey = create_journey_subclass do
|
155
|
+
cancel_if proc_condition
|
156
|
+
|
157
|
+
step do
|
158
|
+
SideEffects.touch! "should not run"
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
journey = canceling_journey.create!
|
163
|
+
assert journey.ready?
|
164
|
+
|
165
|
+
assert_no_side_effects { journey.perform_next_step! }
|
166
|
+
assert journey.canceled?
|
167
|
+
end
|
168
|
+
|
169
|
+
test "cancel_if with conditional object works correctly" do
|
170
|
+
conditional = StepperMotor::Conditional.new(true)
|
171
|
+
|
172
|
+
canceling_journey = create_journey_subclass do
|
173
|
+
cancel_if conditional
|
174
|
+
|
175
|
+
step do
|
176
|
+
SideEffects.touch! "should not run"
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
journey = canceling_journey.create!
|
181
|
+
assert journey.ready?
|
182
|
+
|
183
|
+
assert_no_side_effects { journey.perform_next_step! }
|
184
|
+
assert journey.canceled?
|
185
|
+
end
|
186
|
+
|
187
|
+
test "cancel_if with nil argument does not cancel journey" do
|
188
|
+
canceling_journey = create_journey_subclass do
|
189
|
+
cancel_if nil
|
190
|
+
|
191
|
+
step do
|
192
|
+
SideEffects.touch! "should run"
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
journey = canceling_journey.create!
|
197
|
+
assert journey.ready?
|
198
|
+
|
199
|
+
assert_produced_side_effects("should run") do
|
200
|
+
journey.perform_next_step!
|
201
|
+
end
|
202
|
+
assert journey.finished?
|
203
|
+
end
|
204
|
+
|
205
|
+
test "multiple cancel_if calls are all evaluated" do
|
206
|
+
canceling_journey = create_journey_subclass do
|
207
|
+
cancel_if false
|
208
|
+
cancel_if true
|
209
|
+
|
210
|
+
step do
|
211
|
+
SideEffects.touch! "should not run"
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
journey = canceling_journey.create!
|
216
|
+
assert journey.ready?
|
217
|
+
|
218
|
+
assert_no_side_effects { journey.perform_next_step! }
|
219
|
+
assert journey.canceled?
|
220
|
+
end
|
221
|
+
|
222
|
+
test "cancel_if conditions are evaluated after setting state to performing" do
|
223
|
+
canceling_journey = create_journey_subclass do
|
224
|
+
cancel_if { performing? }
|
225
|
+
|
226
|
+
step do
|
227
|
+
SideEffects.touch! "should not run"
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
journey = canceling_journey.create!
|
232
|
+
assert journey.ready?
|
233
|
+
|
234
|
+
assert_no_side_effects { journey.perform_next_step! }
|
235
|
+
assert journey.canceled?
|
236
|
+
end
|
237
|
+
|
238
|
+
test "cancel_if with no arguments and no block raises error" do
|
239
|
+
assert_raises(ArgumentError, "cancel_if requires either a condition argument or a block") do
|
240
|
+
create_journey_subclass do
|
241
|
+
cancel_if
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
test "cancel_if with both argument and block raises error" do
|
247
|
+
assert_raises(ArgumentError, "cancel_if accepts either a condition argument or a block, but not both") do
|
248
|
+
create_journey_subclass do
|
249
|
+
cancel_if true do
|
250
|
+
false
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
test "cancel_if conditions are class-inheritable" do
|
257
|
+
parent_journey = create_journey_subclass do
|
258
|
+
cancel_if true
|
259
|
+
end
|
260
|
+
|
261
|
+
child_journey = create_journey_subclass(parent_journey) do
|
262
|
+
step do
|
263
|
+
SideEffects.touch! "should not run"
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
journey = child_journey.create!
|
268
|
+
assert journey.ready?
|
269
|
+
|
270
|
+
assert_no_side_effects { journey.perform_next_step! }
|
271
|
+
assert journey.canceled?
|
272
|
+
end
|
273
|
+
|
274
|
+
test "cancel_if conditions are appendable in subclasses" do
|
275
|
+
parent_journey = create_journey_subclass do
|
276
|
+
cancel_if false
|
277
|
+
end
|
278
|
+
|
279
|
+
child_journey = create_journey_subclass(parent_journey) do
|
280
|
+
cancel_if true
|
281
|
+
|
282
|
+
step do
|
283
|
+
SideEffects.touch! "should not run"
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
journey = child_journey.create!
|
288
|
+
assert journey.ready?
|
289
|
+
|
290
|
+
assert_no_side_effects { journey.perform_next_step! }
|
291
|
+
assert journey.canceled?
|
292
|
+
end
|
293
|
+
end
|
@@ -0,0 +1,190 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_helper"
|
4
|
+
|
5
|
+
class WrapConditionalTest < ActiveSupport::TestCase
|
6
|
+
# Validate the skip_if condition
|
7
|
+
# if ![true, false, nil].include?(@skip_if_condition) && !@skip_if_condition.is_a?(Symbol) && !@skip_if_condition.respond_to?(:call)
|
8
|
+
# raise ArgumentError, "skip_if: condition must be a boolean, nil, Symbol or a callable object, but was a #{@skip_if_condition.inspect}"
|
9
|
+
# end
|
10
|
+
|
11
|
+
test "wraps true without negate" do
|
12
|
+
conditional = StepperMotor::Conditional.new(true)
|
13
|
+
assert_equal true, conditional.satisfied_by?(nil)
|
14
|
+
assert_equal true, conditional.satisfied_by?("something")
|
15
|
+
end
|
16
|
+
|
17
|
+
test "wraps true with negate" do
|
18
|
+
conditional = StepperMotor::Conditional.new(true, negate: true)
|
19
|
+
assert_equal false, conditional.satisfied_by?(nil)
|
20
|
+
assert_equal false, conditional.satisfied_by?("something")
|
21
|
+
end
|
22
|
+
|
23
|
+
test "wraps false without negate" do
|
24
|
+
conditional = StepperMotor::Conditional.new(false)
|
25
|
+
assert_equal false, conditional.satisfied_by?(nil)
|
26
|
+
assert_equal false, conditional.satisfied_by?("something")
|
27
|
+
end
|
28
|
+
|
29
|
+
test "wraps false with negate" do
|
30
|
+
conditional = StepperMotor::Conditional.new(false, negate: true)
|
31
|
+
assert_equal true, conditional.satisfied_by?(nil)
|
32
|
+
assert_equal true, conditional.satisfied_by?("something")
|
33
|
+
end
|
34
|
+
|
35
|
+
test "wraps nil without negate" do
|
36
|
+
conditional = StepperMotor::Conditional.new(nil)
|
37
|
+
assert_equal false, conditional.satisfied_by?(nil)
|
38
|
+
assert_equal false, conditional.satisfied_by?("something")
|
39
|
+
end
|
40
|
+
|
41
|
+
test "wraps nil with negate" do
|
42
|
+
conditional = StepperMotor::Conditional.new(nil, negate: true)
|
43
|
+
assert_equal true, conditional.satisfied_by?(nil)
|
44
|
+
assert_equal true, conditional.satisfied_by?("something")
|
45
|
+
end
|
46
|
+
|
47
|
+
test "wraps multiple without negate" do
|
48
|
+
conditional = StepperMotor::Conditional.new([true, true])
|
49
|
+
assert_equal true, conditional.satisfied_by?(nil)
|
50
|
+
|
51
|
+
conditional = StepperMotor::Conditional.new([true, false])
|
52
|
+
assert_equal false, conditional.satisfied_by?(nil)
|
53
|
+
|
54
|
+
conditional = StepperMotor::Conditional.new([false, false])
|
55
|
+
assert_equal false, conditional.satisfied_by?(nil)
|
56
|
+
end
|
57
|
+
|
58
|
+
test "wraps multiple with proc" do
|
59
|
+
conditional = StepperMotor::Conditional.new([true, -> { true }])
|
60
|
+
assert_equal true, conditional.satisfied_by?(nil)
|
61
|
+
|
62
|
+
conditional = StepperMotor::Conditional.new([true, -> { false }])
|
63
|
+
assert_equal false, conditional.satisfied_by?(nil)
|
64
|
+
end
|
65
|
+
|
66
|
+
test "wraps multiple with negate" do
|
67
|
+
conditional = StepperMotor::Conditional.new([true, true], negate: true)
|
68
|
+
assert_equal false, conditional.satisfied_by?(nil)
|
69
|
+
|
70
|
+
conditional = StepperMotor::Conditional.new([true, false], negate: true)
|
71
|
+
assert_equal true, conditional.satisfied_by?(nil)
|
72
|
+
|
73
|
+
conditional = StepperMotor::Conditional.new([false, false], negate: true)
|
74
|
+
assert_equal true, conditional.satisfied_by?(nil)
|
75
|
+
end
|
76
|
+
|
77
|
+
test "wraps callable without negate" do
|
78
|
+
conditional = StepperMotor::Conditional.new(-> { :foo })
|
79
|
+
assert_equal true, conditional.satisfied_by?(nil)
|
80
|
+
|
81
|
+
conditional = StepperMotor::Conditional.new(-> {})
|
82
|
+
assert_equal false, conditional.satisfied_by?(nil)
|
83
|
+
|
84
|
+
conditional = StepperMotor::Conditional.new(-> { false })
|
85
|
+
assert_equal false, conditional.satisfied_by?(nil)
|
86
|
+
|
87
|
+
conditional = StepperMotor::Conditional.new(-> { true })
|
88
|
+
assert_equal true, conditional.satisfied_by?(nil)
|
89
|
+
end
|
90
|
+
|
91
|
+
test "wraps callable with negate" do
|
92
|
+
conditional = StepperMotor::Conditional.new(-> { :foo }, negate: true)
|
93
|
+
assert_equal false, conditional.satisfied_by?(nil)
|
94
|
+
|
95
|
+
conditional = StepperMotor::Conditional.new(-> {}, negate: true)
|
96
|
+
assert_equal true, conditional.satisfied_by?(nil)
|
97
|
+
|
98
|
+
conditional = StepperMotor::Conditional.new(-> { false }, negate: true)
|
99
|
+
assert_equal true, conditional.satisfied_by?(nil)
|
100
|
+
|
101
|
+
conditional = StepperMotor::Conditional.new(-> { true }, negate: true)
|
102
|
+
assert_equal false, conditional.satisfied_by?(nil)
|
103
|
+
end
|
104
|
+
|
105
|
+
class Doer
|
106
|
+
def should_do?
|
107
|
+
true
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
class NotDoer
|
112
|
+
def should_do?
|
113
|
+
false
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
test "wraps method without negate" do
|
118
|
+
doer = Doer.new
|
119
|
+
|
120
|
+
conditional = StepperMotor::Conditional.new(:should_do?)
|
121
|
+
assert_equal true, conditional.satisfied_by?(doer)
|
122
|
+
|
123
|
+
not_doer = NotDoer.new
|
124
|
+
assert_equal false, conditional.satisfied_by?(not_doer)
|
125
|
+
end
|
126
|
+
|
127
|
+
test "wraps method with negate" do
|
128
|
+
doer = Doer.new
|
129
|
+
|
130
|
+
conditional = StepperMotor::Conditional.new(:should_do?, negate: true)
|
131
|
+
assert_equal false, conditional.satisfied_by?(doer)
|
132
|
+
|
133
|
+
not_doer = NotDoer.new
|
134
|
+
assert_equal true, conditional.satisfied_by?(not_doer)
|
135
|
+
end
|
136
|
+
|
137
|
+
test "wraps conditional without negate" do
|
138
|
+
inner_conditional = StepperMotor::Conditional.new(true)
|
139
|
+
outer_conditional = StepperMotor::Conditional.new(inner_conditional)
|
140
|
+
|
141
|
+
assert_equal true, outer_conditional.satisfied_by?(nil)
|
142
|
+
assert_equal true, outer_conditional.satisfied_by?("something")
|
143
|
+
end
|
144
|
+
|
145
|
+
test "wraps conditional with negate" do
|
146
|
+
inner_conditional = StepperMotor::Conditional.new(true)
|
147
|
+
outer_conditional = StepperMotor::Conditional.new(inner_conditional, negate: true)
|
148
|
+
|
149
|
+
assert_equal false, outer_conditional.satisfied_by?(nil)
|
150
|
+
assert_equal false, outer_conditional.satisfied_by?("something")
|
151
|
+
end
|
152
|
+
|
153
|
+
test "wraps negated conditional without negate" do
|
154
|
+
inner_conditional = StepperMotor::Conditional.new(true, negate: true)
|
155
|
+
outer_conditional = StepperMotor::Conditional.new(inner_conditional)
|
156
|
+
|
157
|
+
assert_equal false, outer_conditional.satisfied_by?(nil)
|
158
|
+
assert_equal false, outer_conditional.satisfied_by?("something")
|
159
|
+
end
|
160
|
+
|
161
|
+
test "wraps negated conditional with negate" do
|
162
|
+
inner_conditional = StepperMotor::Conditional.new(true, negate: true)
|
163
|
+
outer_conditional = StepperMotor::Conditional.new(inner_conditional, negate: true)
|
164
|
+
|
165
|
+
assert_equal true, outer_conditional.satisfied_by?(nil)
|
166
|
+
assert_equal true, outer_conditional.satisfied_by?("something")
|
167
|
+
end
|
168
|
+
|
169
|
+
test "wraps conditional with method" do
|
170
|
+
doer = Doer.new
|
171
|
+
inner_conditional = StepperMotor::Conditional.new(:should_do?)
|
172
|
+
outer_conditional = StepperMotor::Conditional.new(inner_conditional)
|
173
|
+
|
174
|
+
assert_equal true, outer_conditional.satisfied_by?(doer)
|
175
|
+
|
176
|
+
not_doer = NotDoer.new
|
177
|
+
assert_equal false, outer_conditional.satisfied_by?(not_doer)
|
178
|
+
end
|
179
|
+
|
180
|
+
test "wraps conditional with method and negate" do
|
181
|
+
doer = Doer.new
|
182
|
+
inner_conditional = StepperMotor::Conditional.new(:should_do?)
|
183
|
+
outer_conditional = StepperMotor::Conditional.new(inner_conditional, negate: true)
|
184
|
+
|
185
|
+
assert_equal false, outer_conditional.satisfied_by?(doer)
|
186
|
+
|
187
|
+
not_doer = NotDoer.new
|
188
|
+
assert_equal true, outer_conditional.satisfied_by?(not_doer)
|
189
|
+
end
|
190
|
+
end
|
data/test/test_helper.rb
CHANGED
@@ -32,11 +32,12 @@ module JourneyDefinitionHelper
|
|
32
32
|
super
|
33
33
|
end
|
34
34
|
|
35
|
-
def create_journey_subclass(&blk)
|
35
|
+
def create_journey_subclass(parent_class = nil, &blk)
|
36
36
|
# https://stackoverflow.com/questions/4113479/dynamic-class-definition-with-a-class-name
|
37
37
|
random_component = @class_names_rng.hex(8)
|
38
38
|
random_name = "JourneySubclass_#{random_component}"
|
39
|
-
|
39
|
+
parent_class ||= StepperMotor::Journey
|
40
|
+
klass = Class.new(parent_class, &blk)
|
40
41
|
Object.const_set(random_name, klass)
|
41
42
|
@dynamic_class_names << random_name
|
42
43
|
klass
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: stepper_motor
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.18
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Julik Tarkhanov
|
@@ -236,6 +236,7 @@ files:
|
|
236
236
|
- lib/generators/stepper_motor_migration_005.rb.erb
|
237
237
|
- lib/stepper_motor.rb
|
238
238
|
- lib/stepper_motor/base_job.rb
|
239
|
+
- lib/stepper_motor/conditional.rb
|
239
240
|
- lib/stepper_motor/cyclic_scheduler.rb
|
240
241
|
- lib/stepper_motor/delete_completed_journeys_job.rb
|
241
242
|
- lib/stepper_motor/forward_scheduler.rb
|
@@ -309,6 +310,7 @@ files:
|
|
309
310
|
- test/stepper_motor/cyclic_scheduler_test.rb
|
310
311
|
- test/stepper_motor/forward_scheduler_test.rb
|
311
312
|
- test/stepper_motor/housekeeping_job_test.rb
|
313
|
+
- test/stepper_motor/journey/cancel_if_test.rb
|
312
314
|
- test/stepper_motor/journey/exception_handling_test.rb
|
313
315
|
- test/stepper_motor/journey/flow_control_test.rb
|
314
316
|
- test/stepper_motor/journey/idempotency_test.rb
|
@@ -320,6 +322,7 @@ files:
|
|
320
322
|
- test/stepper_motor/recover_stuck_journeys_job_test.rb
|
321
323
|
- test/stepper_motor/recovery_test.rb
|
322
324
|
- test/stepper_motor/test_helper_test.rb
|
325
|
+
- test/stepper_motor/wrap_conditional_test.rb
|
323
326
|
- test/stepper_motor_test.rb
|
324
327
|
- test/test_helper.rb
|
325
328
|
homepage: https://steppermotor.dev
|
@@ -337,7 +340,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
337
340
|
requirements:
|
338
341
|
- - ">="
|
339
342
|
- !ruby/object:Gem::Version
|
340
|
-
version:
|
343
|
+
version: 3.1.0
|
341
344
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
342
345
|
requirements:
|
343
346
|
- - ">="
|