switchyard 7.0.0
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/.github/workflows/project-build.yml +71 -0
- data/.gitignore +24 -0
- data/.rspec +3 -0
- data/.rubocop.yml +101 -0
- data/.solargraph.yml +11 -0
- data/Appraisals +7 -0
- data/CHANGELOG.md +303 -0
- data/CODE_OF_CONDUCT.md +22 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +1663 -0
- data/Rakefile +12 -0
- data/VERSION +1 -0
- data/audit/bench.rb +99 -0
- data/audit/verify_findings.rb +172 -0
- data/gemfiles/dry_inflector_0_2_1.gemfile +5 -0
- data/gemfiles/i18n_1_8_11.gemfile +5 -0
- data/lib/switchyard/action.rb +145 -0
- data/lib/switchyard/configuration.rb +36 -0
- data/lib/switchyard/context/key_verifier.rb +124 -0
- data/lib/switchyard/context.rb +208 -0
- data/lib/switchyard/deprecations.rb +26 -0
- data/lib/switchyard/errors.rb +9 -0
- data/lib/switchyard/functional/enum.rb +286 -0
- data/lib/switchyard/functional/maybe.rb +21 -0
- data/lib/switchyard/functional/monad.rb +77 -0
- data/lib/switchyard/functional/null.rb +88 -0
- data/lib/switchyard/functional/option.rb +100 -0
- data/lib/switchyard/functional/result.rb +129 -0
- data/lib/switchyard/functional/sequencer.rb +144 -0
- data/lib/switchyard/i18n/localization_adapter.rb +50 -0
- data/lib/switchyard/localization_adapter.rb +39 -0
- data/lib/switchyard/localization_map.rb +9 -0
- data/lib/switchyard/organizer/execute.rb +16 -0
- data/lib/switchyard/organizer/iterate.rb +30 -0
- data/lib/switchyard/organizer/reduce_case.rb +50 -0
- data/lib/switchyard/organizer/reduce_if.rb +19 -0
- data/lib/switchyard/organizer/reduce_if_else.rb +23 -0
- data/lib/switchyard/organizer/reduce_until.rb +22 -0
- data/lib/switchyard/organizer/reduce_while.rb +31 -0
- data/lib/switchyard/organizer/scoped_reducable.rb +15 -0
- data/lib/switchyard/organizer/with_callback.rb +28 -0
- data/lib/switchyard/organizer/with_reducer.rb +81 -0
- data/lib/switchyard/organizer/with_reducer_factory.rb +20 -0
- data/lib/switchyard/organizer/with_reducer_log_decorator.rb +111 -0
- data/lib/switchyard/organizer.rb +129 -0
- data/lib/switchyard/testing/context_factory.rb +48 -0
- data/lib/switchyard/testing.rb +3 -0
- data/lib/switchyard/version.rb +5 -0
- data/lib/switchyard.rb +36 -0
- data/resources/fail_actions.png +0 -0
- data/resources/light-service.png +0 -0
- data/resources/organizer_and_actions.png +0 -0
- data/resources/skip_actions.png +0 -0
- data/spec/acceptance/add_numbers_spec.rb +11 -0
- data/spec/acceptance/after_actions_spec.rb +87 -0
- data/spec/acceptance/around_each_spec.rb +19 -0
- data/spec/acceptance/before_actions_spec.rb +115 -0
- data/spec/acceptance/custom_log_from_organizer_spec.rb +61 -0
- data/spec/acceptance/deprecation_warnings_spec.rb +82 -0
- data/spec/acceptance/fail_spec.rb +52 -0
- data/spec/acceptance/log_from_organizer_spec.rb +154 -0
- data/spec/acceptance/message_localization_spec.rb +119 -0
- data/spec/acceptance/organizer/add_aliases_spec.rb +28 -0
- data/spec/acceptance/organizer/add_to_context_spec.rb +54 -0
- data/spec/acceptance/organizer/around_each_with_reduce_if_spec.rb +42 -0
- data/spec/acceptance/organizer/context_failure_and_skipping_spec.rb +90 -0
- data/spec/acceptance/organizer/execute_spec.rb +67 -0
- data/spec/acceptance/organizer/iterate_spec.rb +44 -0
- data/spec/acceptance/organizer/reduce_case_spec.rb +65 -0
- data/spec/acceptance/organizer/reduce_if_else_spec.rb +60 -0
- data/spec/acceptance/organizer/reduce_if_spec.rb +89 -0
- data/spec/acceptance/organizer/reduce_until_spec.rb +49 -0
- data/spec/acceptance/organizer/reduce_while_spec.rb +96 -0
- data/spec/acceptance/organizer/with_callback_spec.rb +113 -0
- data/spec/acceptance/organizer_entry_point_spec.rb +35 -0
- data/spec/acceptance/rollback_spec.rb +183 -0
- data/spec/acceptance/skip_all_remaining_spec.rb +139 -0
- data/spec/acceptance/testing/context_factory_spec.rb +54 -0
- data/spec/action_expected_keys_spec.rb +63 -0
- data/spec/action_expects_and_promises_spec.rb +97 -0
- data/spec/action_optional_expected_keys_spec.rb +107 -0
- data/spec/action_promised_keys_spec.rb +126 -0
- data/spec/action_spec.rb +97 -0
- data/spec/context/inspect_spec.rb +52 -0
- data/spec/context_spec.rb +301 -0
- data/spec/examples/amount_spec.rb +77 -0
- data/spec/examples/controller_spec.rb +63 -0
- data/spec/examples/validate_address_spec.rb +38 -0
- data/spec/i18n_localization_adapter_spec.rb +83 -0
- data/spec/lib/deterministic/class_mixin_spec.rb +24 -0
- data/spec/lib/deterministic/currify_spec.rb +90 -0
- data/spec/lib/deterministic/monad_axioms.rb +46 -0
- data/spec/lib/deterministic/monad_spec.rb +47 -0
- data/spec/lib/deterministic/null_spec.rb +65 -0
- data/spec/lib/deterministic/option_spec.rb +140 -0
- data/spec/lib/deterministic/result/failure_spec.rb +65 -0
- data/spec/lib/deterministic/result/result_map_spec.rb +155 -0
- data/spec/lib/deterministic/result/result_shared.rb +25 -0
- data/spec/lib/deterministic/result/success_spec.rb +41 -0
- data/spec/lib/deterministic/result_spec.rb +63 -0
- data/spec/lib/deterministic/sequencer_spec.rb +506 -0
- data/spec/lib/edge_cases_spec.rb +156 -0
- data/spec/lib/enum_spec.rb +114 -0
- data/spec/lib/native_pattern_matching_spec.rb +74 -0
- data/spec/localization_adapter_spec.rb +66 -0
- data/spec/organizer/with_reducer_spec.rb +56 -0
- data/spec/organizer_key_aliases_spec.rb +29 -0
- data/spec/organizer_spec.rb +115 -0
- data/spec/readme_spec.rb +45 -0
- data/spec/sample/calculates_order_tax_action_spec.rb +16 -0
- data/spec/sample/calculates_tax_spec.rb +30 -0
- data/spec/sample/looks_up_tax_percentage_action_spec.rb +55 -0
- data/spec/sample/provides_free_shipping_action_spec.rb +25 -0
- data/spec/sample/tax/calculates_order_tax_action.rb +10 -0
- data/spec/sample/tax/calculates_tax.rb +11 -0
- data/spec/sample/tax/looks_up_tax_percentage_action.rb +28 -0
- data/spec/sample/tax/provides_free_shipping_action.rb +11 -0
- data/spec/spec_helper.rb +32 -0
- data/spec/support.rb +1 -0
- data/spec/test_doubles.rb +656 -0
- data/spec/testing/context_factory/iterate_spec.rb +39 -0
- data/spec/testing/context_factory/reduce_if_spec.rb +40 -0
- data/spec/testing/context_factory/reduce_until_spec.rb +40 -0
- data/spec/testing/context_factory/with_callback_spec.rb +38 -0
- data/spec/testing/context_factory_spec.rb +76 -0
- data/switchyard.gemspec +35 -0
- metadata +351 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'test_doubles'
|
|
3
|
+
|
|
4
|
+
RSpec.describe Switchyard::Organizer do
|
|
5
|
+
describe 'a simple case with a single callback' do
|
|
6
|
+
it 'calls the actions defined with callback' do
|
|
7
|
+
result = TestDoubles::TestWithCallback.call
|
|
8
|
+
|
|
9
|
+
expect(result.counter).to eq(3)
|
|
10
|
+
expect(result.total).to eq(6)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
describe 'a more complex example with nested callbacks' do
|
|
15
|
+
class TestWithNestedCallback
|
|
16
|
+
extend Switchyard::Organizer
|
|
17
|
+
|
|
18
|
+
def self.call(context = {})
|
|
19
|
+
with(context).reduce(actions)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.actions
|
|
23
|
+
[
|
|
24
|
+
SetUpNestedContextAction,
|
|
25
|
+
with_callback(IterateOuterCollectionAction,
|
|
26
|
+
[IncrementOuterCountAction,
|
|
27
|
+
with_callback(TestDoubles::IterateCollectionAction,
|
|
28
|
+
[TestDoubles::IncrementCountAction,
|
|
29
|
+
TestDoubles::AddToTotalAction])])
|
|
30
|
+
]
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class SetUpNestedContextAction
|
|
35
|
+
extend Switchyard::Action
|
|
36
|
+
|
|
37
|
+
promises :outer_numbers, :outer_counter,
|
|
38
|
+
:numbers, :counter, :total
|
|
39
|
+
|
|
40
|
+
executed do |ctx|
|
|
41
|
+
ctx.outer_numbers = [12, 17]
|
|
42
|
+
ctx.outer_counter = 0
|
|
43
|
+
ctx.numbers = [1, 2, 3]
|
|
44
|
+
ctx.counter = 0
|
|
45
|
+
ctx.total = 0
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
class IterateOuterCollectionAction
|
|
50
|
+
extend Switchyard::Action
|
|
51
|
+
|
|
52
|
+
expects :outer_numbers, :callback
|
|
53
|
+
promises :outer_number
|
|
54
|
+
|
|
55
|
+
executed do |ctx|
|
|
56
|
+
ctx.outer_numbers.each do |outer_number|
|
|
57
|
+
ctx.outer_number = outer_number
|
|
58
|
+
ctx.callback.call(ctx)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
class IncrementOuterCountAction
|
|
64
|
+
extend Switchyard::Action
|
|
65
|
+
|
|
66
|
+
expects :outer_counter
|
|
67
|
+
|
|
68
|
+
executed do |ctx|
|
|
69
|
+
ctx.outer_counter = ctx.outer_counter + 1
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it 'calls both the action and the nested callbacks' do
|
|
74
|
+
result = TestWithNestedCallback.call
|
|
75
|
+
|
|
76
|
+
expect(result.outer_counter).to eq(2)
|
|
77
|
+
# Counts and total are the duplicates of
|
|
78
|
+
# what you'll see in the simple spec,
|
|
79
|
+
# as the internal callback logic is called
|
|
80
|
+
# twice due to 2 items in the outer_numbers
|
|
81
|
+
# collection.
|
|
82
|
+
expect(result.counter).to eq(6)
|
|
83
|
+
expect(result.total).to eq(12)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
describe 'with failed or skipped context' do
|
|
88
|
+
class TestWithFailureCallback
|
|
89
|
+
extend Switchyard::Organizer
|
|
90
|
+
|
|
91
|
+
def self.call(context = {})
|
|
92
|
+
with(context).reduce(actions)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def self.actions
|
|
96
|
+
[
|
|
97
|
+
TestDoubles::SetUpContextAction,
|
|
98
|
+
with_callback(TestDoubles::IterateCollectionAction,
|
|
99
|
+
[TestDoubles::IncrementCountAction,
|
|
100
|
+
TestDoubles::FailureAction])
|
|
101
|
+
]
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it 'will not process the routine' do
|
|
106
|
+
result = TestWithFailureCallback.call
|
|
107
|
+
|
|
108
|
+
expect(result).to be_failure
|
|
109
|
+
expect(result.counter).to eq(1)
|
|
110
|
+
expect(result.total).to eq(0)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'test_doubles'
|
|
3
|
+
|
|
4
|
+
describe "Organizer entry point" do
|
|
5
|
+
context "when the organizer entry method is not named `call`" do
|
|
6
|
+
it "works without emitting any warning" do
|
|
7
|
+
class OrganizerWithoutCallMethod
|
|
8
|
+
extend Switchyard::Organizer
|
|
9
|
+
|
|
10
|
+
def self.do_something
|
|
11
|
+
reduce([])
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
result = nil
|
|
16
|
+
expect { result = OrganizerWithoutCallMethod.do_something }
|
|
17
|
+
.not_to output.to_stdout
|
|
18
|
+
expect(result).to be_a_kind_of(Switchyard::Context)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
context "when the organizer has the `call` method" do
|
|
23
|
+
it "works without emitting any warning" do
|
|
24
|
+
class OrganizerWithCallMethod
|
|
25
|
+
extend Switchyard::Organizer
|
|
26
|
+
|
|
27
|
+
def self.call
|
|
28
|
+
reduce([])
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
expect(OrganizerWithCallMethod.call).to be_a_kind_of(Switchyard::Context)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'test_doubles'
|
|
3
|
+
|
|
4
|
+
class RollbackOrganizer
|
|
5
|
+
extend Switchyard::Organizer
|
|
6
|
+
|
|
7
|
+
def self.call(number)
|
|
8
|
+
with(:number => number).reduce(
|
|
9
|
+
AddsOneWithRollbackAction,
|
|
10
|
+
TestDoubles::AddsTwoAction,
|
|
11
|
+
AddsThreeWithRollbackAction
|
|
12
|
+
)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class AddsOneWithRollbackAction
|
|
17
|
+
extend Switchyard::Action
|
|
18
|
+
|
|
19
|
+
expects :number
|
|
20
|
+
promises :number
|
|
21
|
+
|
|
22
|
+
executed do |context|
|
|
23
|
+
context.fail_with_rollback! if context.number.zero?
|
|
24
|
+
|
|
25
|
+
context.number += 1
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
rolled_back do |context|
|
|
29
|
+
context.number -= 1
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class AddsThreeWithRollbackAction
|
|
34
|
+
extend Switchyard::Action
|
|
35
|
+
|
|
36
|
+
expects :number
|
|
37
|
+
|
|
38
|
+
executed do |context|
|
|
39
|
+
context.number = context.number + 3
|
|
40
|
+
|
|
41
|
+
context.fail_with_rollback!("I did not like this!")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
rolled_back do |context|
|
|
45
|
+
context.number -= 3
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
class RollbackOrganizerWithNoRollback
|
|
50
|
+
extend Switchyard::Organizer
|
|
51
|
+
|
|
52
|
+
def self.call(number)
|
|
53
|
+
with(:number => number).reduce(
|
|
54
|
+
TestDoubles::AddsOneAction,
|
|
55
|
+
TestDoubles::AddsTwoAction,
|
|
56
|
+
AddsThreeWithNoRollbackAction
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
class AddsThreeWithNoRollbackAction
|
|
62
|
+
extend Switchyard::Action
|
|
63
|
+
|
|
64
|
+
expects :number
|
|
65
|
+
|
|
66
|
+
executed do |context|
|
|
67
|
+
context.number = context.number + 3
|
|
68
|
+
|
|
69
|
+
context.fail_with_rollback!("I did not like this!")
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
class RollbackOrganizerWithMiddleRollback
|
|
74
|
+
extend Switchyard::Organizer
|
|
75
|
+
|
|
76
|
+
def self.call(number)
|
|
77
|
+
with(:number => number).reduce(
|
|
78
|
+
TestDoubles::AddsOneAction,
|
|
79
|
+
AddsTwoActionWithRollback,
|
|
80
|
+
TestDoubles::AddsThreeAction
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
class AddsTwoActionWithRollback
|
|
86
|
+
extend Switchyard::Action
|
|
87
|
+
|
|
88
|
+
expects :number
|
|
89
|
+
|
|
90
|
+
executed do |context|
|
|
91
|
+
context.number = context.number + 2
|
|
92
|
+
|
|
93
|
+
context.fail_with_rollback!("I did not like this a bit!")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
rolled_back do |context|
|
|
97
|
+
context.number -= 2
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
class RollbackOrganizerWithDuplicatedAction
|
|
102
|
+
extend Switchyard::Organizer
|
|
103
|
+
|
|
104
|
+
def self.call(ctx)
|
|
105
|
+
with(ctx).reduce(
|
|
106
|
+
TracksRollbackAction,
|
|
107
|
+
FailsOnSecondRunWithRollbackAction,
|
|
108
|
+
TracksRollbackAction,
|
|
109
|
+
FailsOnSecondRunWithRollbackAction
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
class TracksRollbackAction
|
|
115
|
+
extend Switchyard::Action
|
|
116
|
+
|
|
117
|
+
executed do |context|
|
|
118
|
+
(context[:executed] ||= []) << :tracks
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
rolled_back do |context|
|
|
122
|
+
(context[:rolled_back] ||= []) << :tracks
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
class FailsOnSecondRunWithRollbackAction
|
|
127
|
+
extend Switchyard::Action
|
|
128
|
+
|
|
129
|
+
executed do |context|
|
|
130
|
+
(context[:executed] ||= []) << :fails
|
|
131
|
+
context.fail_with_rollback!("boom") if context[:executed].count(:fails) == 2
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
rolled_back do |context|
|
|
135
|
+
(context[:rolled_back] ||= []) << :fails
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
describe "Rolling back actions when there is a failure" do
|
|
140
|
+
it "Adds 1, 2, 3 to 1 and rolls back " do
|
|
141
|
+
result = RollbackOrganizer.call 1
|
|
142
|
+
number = result.fetch(:number)
|
|
143
|
+
|
|
144
|
+
expect(result).to be_failure
|
|
145
|
+
expect(result.message).to eq("I did not like this!")
|
|
146
|
+
expect(number).to eq(3)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
it "won't error out when actions don't define rollback" do
|
|
150
|
+
result = RollbackOrganizerWithNoRollback.call 1
|
|
151
|
+
number = result.fetch(:number)
|
|
152
|
+
|
|
153
|
+
expect(result).to be_failure
|
|
154
|
+
expect(result.message).to eq("I did not like this!")
|
|
155
|
+
expect(number).to eq(7)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
it "rolls back properly when triggered with an action in the middle" do
|
|
159
|
+
result = RollbackOrganizerWithMiddleRollback.call 1
|
|
160
|
+
number = result.fetch(:number)
|
|
161
|
+
|
|
162
|
+
expect(result).to be_failure
|
|
163
|
+
expect(result.message).to eq("I did not like this a bit!")
|
|
164
|
+
expect(number).to eq(2)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
it "rolls back from the first action" do
|
|
168
|
+
result = RollbackOrganizer.call 0
|
|
169
|
+
number = result.fetch(:number)
|
|
170
|
+
|
|
171
|
+
expect(result).to be_failure
|
|
172
|
+
expect(number).to eq(-1)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
it "rolls back every executed action when the same action appears twice" do
|
|
176
|
+
result = RollbackOrganizerWithDuplicatedAction.call({})
|
|
177
|
+
|
|
178
|
+
expect(result).to be_failure
|
|
179
|
+
expect(result[:executed]).to eq(%i[tracks fails tracks fails])
|
|
180
|
+
# tutte e 4 le azioni eseguite vengono compensate, in ordine inverso
|
|
181
|
+
expect(result[:rolled_back]).to eq(%i[fails tracks fails tracks])
|
|
182
|
+
end
|
|
183
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
RSpec.describe "skip_all_remaining!" do
|
|
4
|
+
context "with regular organizer" do
|
|
5
|
+
let(:organizer) do
|
|
6
|
+
Class.new do
|
|
7
|
+
extend Switchyard::Organizer
|
|
8
|
+
|
|
9
|
+
def self.call(ctx)
|
|
10
|
+
with(ctx).reduce(actions)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.actions
|
|
14
|
+
[
|
|
15
|
+
execute(->(c) { c[:first] = true }),
|
|
16
|
+
execute(lambda(&:skip_all_remaining!)),
|
|
17
|
+
execute(->(c) { c[:second] = true })
|
|
18
|
+
]
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "skips all remaining actions" do
|
|
24
|
+
result = organizer.call(Switchyard::Context.make)
|
|
25
|
+
|
|
26
|
+
expect(result[:first]).to be true
|
|
27
|
+
expect(result[:second]).to be_nil
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
context "with an organizer with a reducer" do
|
|
32
|
+
let(:organizer) do
|
|
33
|
+
Class.new do
|
|
34
|
+
extend Switchyard::Organizer
|
|
35
|
+
|
|
36
|
+
def self.call(ctx)
|
|
37
|
+
with(ctx).reduce(actions)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.actions
|
|
41
|
+
[
|
|
42
|
+
reduce_if(
|
|
43
|
+
->(_) { true },
|
|
44
|
+
[
|
|
45
|
+
execute(->(c) { c[:first_inside] = true }),
|
|
46
|
+
execute(lambda(&:skip_all_remaining!)),
|
|
47
|
+
execute(->(c) { c[:second_inside] = true })
|
|
48
|
+
]
|
|
49
|
+
),
|
|
50
|
+
execute(->(c) { c[:outside] = true })
|
|
51
|
+
]
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "skips all remaining actions inside and outside the reducer" do
|
|
57
|
+
result = organizer.call(Switchyard::Context.make)
|
|
58
|
+
|
|
59
|
+
expect(result[:first_inside]).to be true
|
|
60
|
+
expect(result[:second_inside]).to be_nil
|
|
61
|
+
expect(result[:outside]).to be_nil
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
context "with a message" do
|
|
66
|
+
let(:organizer) do
|
|
67
|
+
Class.new do
|
|
68
|
+
extend Switchyard::Organizer
|
|
69
|
+
|
|
70
|
+
def self.call(ctx)
|
|
71
|
+
with(ctx).reduce(actions)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def self.actions
|
|
75
|
+
[
|
|
76
|
+
reduce_if(
|
|
77
|
+
->(_) { true },
|
|
78
|
+
[
|
|
79
|
+
execute(->(c) { c.skip_all_remaining!("Skipping with message") })
|
|
80
|
+
]
|
|
81
|
+
),
|
|
82
|
+
execute(->(c) { c[:outside] = true })
|
|
83
|
+
]
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it "preserves the message when exiting scoped reducers" do
|
|
89
|
+
result = organizer.call(Switchyard::Context.make)
|
|
90
|
+
|
|
91
|
+
expect(result.message).to eq("Skipping with message")
|
|
92
|
+
expect(result[:outside]).to be_nil
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
context "with an organizer with nested reducers" do
|
|
97
|
+
let(:organizer) do
|
|
98
|
+
Class.new do
|
|
99
|
+
extend Switchyard::Organizer
|
|
100
|
+
|
|
101
|
+
def self.call(ctx)
|
|
102
|
+
with(ctx).reduce(actions)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def self.actions # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
|
106
|
+
[
|
|
107
|
+
reduce_if(
|
|
108
|
+
->(_) { true },
|
|
109
|
+
[
|
|
110
|
+
iterate(:items, [
|
|
111
|
+
execute(->(c) {
|
|
112
|
+
c[:executed_items] ||= []
|
|
113
|
+
c[:executed_items] << c[:item]
|
|
114
|
+
}),
|
|
115
|
+
execute(->(c) { c.skip_all_remaining! if c[:item] == 2 }),
|
|
116
|
+
execute(->(c) {
|
|
117
|
+
c[:skipped_in_iterate] ||= []
|
|
118
|
+
c[:skipped_in_iterate] << c[:item]
|
|
119
|
+
})
|
|
120
|
+
]),
|
|
121
|
+
execute(->(c) { c[:after_iterate_inside_if] = true })
|
|
122
|
+
]
|
|
123
|
+
),
|
|
124
|
+
execute(->(c) { c[:outside] = true })
|
|
125
|
+
]
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it "skips all remaining actions across all nested scopes" do
|
|
131
|
+
result = organizer.call(Switchyard::Context.make(:items => [1, 2, 3]))
|
|
132
|
+
|
|
133
|
+
expect(result[:executed_items]).to eq([1, 2])
|
|
134
|
+
expect(result[:skipped_in_iterate]).to eq([1])
|
|
135
|
+
expect(result[:after_iterate_inside_if]).to be_nil
|
|
136
|
+
expect(result[:outside]).to be_nil
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'test_doubles'
|
|
3
|
+
|
|
4
|
+
class AdditionOrganizerContextFactory
|
|
5
|
+
def self.make_for(action, number)
|
|
6
|
+
number += 3 # You can add more logic to prepare your context
|
|
7
|
+
|
|
8
|
+
Switchyard::Testing::ContextFactory
|
|
9
|
+
.make_from(TestDoubles::AdditionOrganizer)
|
|
10
|
+
.for(action)
|
|
11
|
+
.with(number)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
RSpec.describe TestDoubles::AddsThreeAction do
|
|
16
|
+
it 'creates a context for the action with ContextFactory wrapper' do
|
|
17
|
+
context =
|
|
18
|
+
AdditionOrganizerContextFactory
|
|
19
|
+
.make_for(TestDoubles::AddsThreeAction, 1)
|
|
20
|
+
|
|
21
|
+
expect(context.number).to eq(7)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'creates a context for the action using the ContextFactory' do
|
|
25
|
+
context =
|
|
26
|
+
Switchyard::Testing::ContextFactory
|
|
27
|
+
.make_from(TestDoubles::AdditionOrganizer)
|
|
28
|
+
.for(TestDoubles::AddsThreeAction)
|
|
29
|
+
.with(4) # Context is a "glorified" hash
|
|
30
|
+
|
|
31
|
+
expect(context.number).to eq(7)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "works with multiple arguments passed to Organizer's call method" do
|
|
35
|
+
context = Switchyard::Testing::ContextFactory
|
|
36
|
+
.make_from(TestDoubles::ExtraArgumentAdditionOrganizer)
|
|
37
|
+
.for(described_class)
|
|
38
|
+
.with(4, 2)
|
|
39
|
+
|
|
40
|
+
expect(context.number).to eq(9)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
RSpec.describe TestDoubles::AddsTwoAction do
|
|
45
|
+
it 'does not execute a callback entirely from a ContextFactory' do
|
|
46
|
+
context = Switchyard::Testing::ContextFactory
|
|
47
|
+
.make_from(TestDoubles::CallbackOrganizer)
|
|
48
|
+
.for(described_class)
|
|
49
|
+
.with(:number => 0)
|
|
50
|
+
|
|
51
|
+
# add 1, add 10, then stop before executing first add 2
|
|
52
|
+
expect(context.number).to eq(11)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'test_doubles'
|
|
3
|
+
|
|
4
|
+
describe ":expects macro" do
|
|
5
|
+
context "when expected keys are in the context" do
|
|
6
|
+
it "can access the keys as class methods" do
|
|
7
|
+
resulting_context = TestDoubles::MakesTeaWithMilkAction.execute(
|
|
8
|
+
:tea => "black",
|
|
9
|
+
:milk => "full cream",
|
|
10
|
+
:something => "else"
|
|
11
|
+
)
|
|
12
|
+
expect(resulting_context[:milk_tea]).to eq("black - full cream")
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
context "when an expected key is not in the context" do
|
|
17
|
+
it "raises an Switchyard::ExpectedKeysNotInContextError" do
|
|
18
|
+
exception_msg = "expected :milk to be in the context during " \
|
|
19
|
+
"TestDoubles::MakesTeaWithMilkAction"
|
|
20
|
+
expect do
|
|
21
|
+
TestDoubles::MakesTeaWithMilkAction.execute(:tea => "black")
|
|
22
|
+
end.to \
|
|
23
|
+
raise_error(Switchyard::ExpectedKeysNotInContextError, exception_msg)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
context "when the `expects` macro is called multiple times" do
|
|
28
|
+
it "can collect expected keys " do
|
|
29
|
+
result = TestDoubles::MultipleExpectsAction.execute(
|
|
30
|
+
:tea => "black",
|
|
31
|
+
:milk => "full cream",
|
|
32
|
+
:chocolate => "dark chocolate"
|
|
33
|
+
)
|
|
34
|
+
expect(result[:milk_tea]).to \
|
|
35
|
+
eq("black - full cream - with dark chocolate")
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
context "when a reserved key is listed as an expected key" do
|
|
40
|
+
it "raises an error indicating a reserved key is expected" do
|
|
41
|
+
exception_msg = "promised or expected keys cannot be a reserved key: " \
|
|
42
|
+
"[:message]"
|
|
43
|
+
expect do
|
|
44
|
+
TestDoubles::MakesTeaExpectingReservedKey.execute(:tea => "black",
|
|
45
|
+
:message => "no no")
|
|
46
|
+
end.to \
|
|
47
|
+
raise_error(Switchyard::ReservedKeysInContextError, exception_msg)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it "raises an error indicating that multiple reserved keys are expected" do
|
|
51
|
+
exception_msg = "promised or expected keys cannot be a reserved key: " \
|
|
52
|
+
"[:message, :error_code, :current_action]"
|
|
53
|
+
expect do
|
|
54
|
+
TestDoubles::MakesTeaExpectingMultipleReservedKeys
|
|
55
|
+
.execute(:tea => "black",
|
|
56
|
+
:message => "no no",
|
|
57
|
+
:error_code => 1,
|
|
58
|
+
:current_action => "update")
|
|
59
|
+
end.to raise_error(Switchyard::ReservedKeysInContextError,
|
|
60
|
+
exception_msg)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe ":expects and :promises macros" do
|
|
4
|
+
describe "actions are backward compatible" do
|
|
5
|
+
class FooAction
|
|
6
|
+
extend Switchyard::Action
|
|
7
|
+
|
|
8
|
+
executed do |context|
|
|
9
|
+
baz = context.fetch :baz
|
|
10
|
+
|
|
11
|
+
bar = baz + 2
|
|
12
|
+
context[:bar] = bar
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
it "works without expects and promises" do
|
|
16
|
+
result = FooAction.execute(:baz => 3)
|
|
17
|
+
expect(result).to be_success
|
|
18
|
+
expect(result[:bar]).to eq(5)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
context "when expected keys are not in context" do
|
|
23
|
+
class FooNoExpectedKeyAction
|
|
24
|
+
extend Switchyard::Action
|
|
25
|
+
|
|
26
|
+
expects :baz
|
|
27
|
+
|
|
28
|
+
executed do |context|
|
|
29
|
+
baz = context.fetch :baz
|
|
30
|
+
|
|
31
|
+
bar = baz + 2
|
|
32
|
+
context[:bar] = bar
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
it "throws an ExpectedKeysNotInContextError" do
|
|
36
|
+
# FooAction invoked with nothing in the context
|
|
37
|
+
expect { FooNoExpectedKeyAction.execute }.to \
|
|
38
|
+
raise_error(Switchyard::ExpectedKeysNotInContextError)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
describe "expected keys" do
|
|
43
|
+
class FooWithReaderAction
|
|
44
|
+
extend Switchyard::Action
|
|
45
|
+
|
|
46
|
+
expects :baz
|
|
47
|
+
|
|
48
|
+
executed do |context|
|
|
49
|
+
# Notice how I use `context.baz` here
|
|
50
|
+
bar = context.baz + 2
|
|
51
|
+
context[:bar] = bar
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
it "can be accessed through a reader" do
|
|
55
|
+
result = FooWithReaderAction.execute(:baz => 3)
|
|
56
|
+
expect(result).to be_success
|
|
57
|
+
expect(result[:bar]).to eq(5)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
context "when promised keys are not in context" do
|
|
62
|
+
class FooNoPromisedKeyAction
|
|
63
|
+
extend Switchyard::Action
|
|
64
|
+
|
|
65
|
+
expects :baz
|
|
66
|
+
promises :bar
|
|
67
|
+
|
|
68
|
+
executed do |context|
|
|
69
|
+
# I am not adding anything to the context
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
it "throws a PromisedKeysNotInContextError" do
|
|
73
|
+
# FooAction invoked with nothing placed in the context
|
|
74
|
+
expect { FooNoPromisedKeyAction.execute(:baz => 3) }.to \
|
|
75
|
+
raise_error(Switchyard::PromisedKeysNotInContextError)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
describe "promised keys" do
|
|
80
|
+
class FooWithExpectsAndPromisesAction
|
|
81
|
+
extend Switchyard::Action
|
|
82
|
+
|
|
83
|
+
expects :baz
|
|
84
|
+
promises :bar
|
|
85
|
+
|
|
86
|
+
executed do |context|
|
|
87
|
+
# Notice how I use `context.bar` here
|
|
88
|
+
context.bar = context.baz + 2
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
it "puts the value through the accessor into the context" do
|
|
92
|
+
result = FooWithExpectsAndPromisesAction.execute(:baz => 3)
|
|
93
|
+
expect(result).to be_success
|
|
94
|
+
expect(result[:bar]).to eq(5)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|