operandi 5.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/.cursor/rules/services/RULE.md +269 -0
- data/.cursor/rules/services-rspec/RULE.md +354 -0
- data/.github/dependabot.yml +11 -0
- data/.github/workflows/ci.yml +77 -0
- data/.gitignore +25 -0
- data/.rspec +3 -0
- data/.rubocop.yml +134 -0
- data/.ruby-version +1 -0
- data/.vscode/cspell.json +18 -0
- data/.vscode/project-words.txt +12 -0
- data/AGENTS.md +139 -0
- data/CHANGELOG.md +111 -0
- data/CLAUDE.md +139 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +28 -0
- data/Gemfile.lock +149 -0
- data/LICENSE.txt +21 -0
- data/README.md +172 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/config/default.yml +57 -0
- data/docs/README.md +105 -0
- data/docs/SUMMARY.md +31 -0
- data/docs/arguments.md +275 -0
- data/docs/best-practices.md +153 -0
- data/docs/callbacks.md +475 -0
- data/docs/concepts.md +79 -0
- data/docs/configuration.md +218 -0
- data/docs/context.md +128 -0
- data/docs/crud.md +525 -0
- data/docs/errors.md +331 -0
- data/docs/generators.md +250 -0
- data/docs/outputs.md +150 -0
- data/docs/pundit-authorization.md +320 -0
- data/docs/quickstart.md +133 -0
- data/docs/recipes.md +14 -0
- data/docs/rubocop.md +430 -0
- data/docs/ruby-lsp.md +121 -0
- data/docs/service-rendering.md +222 -0
- data/docs/sorbet-runtime.md +283 -0
- data/docs/steps.md +438 -0
- data/docs/tapioca.md +188 -0
- data/docs/testing.md +548 -0
- data/lib/generators/operandi/install/USAGE +15 -0
- data/lib/generators/operandi/install/install_generator.rb +45 -0
- data/lib/generators/operandi/install/templates/application_service.rb.tt +8 -0
- data/lib/generators/operandi/install/templates/application_service_spec.rb.tt +7 -0
- data/lib/generators/operandi/install/templates/initializer.rb.tt +30 -0
- data/lib/generators/operandi/service/USAGE +21 -0
- data/lib/generators/operandi/service/service_generator.rb +80 -0
- data/lib/generators/operandi/service/templates/service.rb.tt +48 -0
- data/lib/generators/operandi/service/templates/service_spec.rb.tt +40 -0
- data/lib/operandi/base.rb +230 -0
- data/lib/operandi/base_with_context.rb +57 -0
- data/lib/operandi/callbacks.rb +353 -0
- data/lib/operandi/collection.rb +166 -0
- data/lib/operandi/concerns/execution.rb +80 -0
- data/lib/operandi/concerns/parent_service.rb +32 -0
- data/lib/operandi/concerns/state_management.rb +34 -0
- data/lib/operandi/config.rb +142 -0
- data/lib/operandi/constants.rb +96 -0
- data/lib/operandi/dsl/arguments_dsl.rb +83 -0
- data/lib/operandi/dsl/outputs_dsl.rb +79 -0
- data/lib/operandi/dsl/steps_dsl.rb +206 -0
- data/lib/operandi/dsl/validation.rb +171 -0
- data/lib/operandi/exceptions.rb +66 -0
- data/lib/operandi/message.rb +52 -0
- data/lib/operandi/messages.rb +185 -0
- data/lib/operandi/rspec/matchers/define_argument.rb +172 -0
- data/lib/operandi/rspec/matchers/define_output.rb +145 -0
- data/lib/operandi/rspec/matchers/define_step.rb +223 -0
- data/lib/operandi/rspec/matchers/execute_step.rb +228 -0
- data/lib/operandi/rspec/matchers/have_error_on.rb +144 -0
- data/lib/operandi/rspec/matchers/have_warning_on.rb +146 -0
- data/lib/operandi/rspec/matchers/trigger_callback.rb +136 -0
- data/lib/operandi/rspec.rb +15 -0
- data/lib/operandi/rubocop/cop/operandi/argument_type_required.rb +52 -0
- data/lib/operandi/rubocop/cop/operandi/condition_method_exists.rb +173 -0
- data/lib/operandi/rubocop/cop/operandi/deprecated_accessors.rb +113 -0
- data/lib/operandi/rubocop/cop/operandi/deprecated_methods.rb +113 -0
- data/lib/operandi/rubocop/cop/operandi/dsl_order.rb +181 -0
- data/lib/operandi/rubocop/cop/operandi/missing_private_keyword.rb +102 -0
- data/lib/operandi/rubocop/cop/operandi/no_direct_instantiation.rb +66 -0
- data/lib/operandi/rubocop/cop/operandi/no_hash_argument.rb +101 -0
- data/lib/operandi/rubocop/cop/operandi/output_type_required.rb +52 -0
- data/lib/operandi/rubocop/cop/operandi/prefer_fail_method.rb +112 -0
- data/lib/operandi/rubocop/cop/operandi/prefer_optional_over_default_nil.rb +124 -0
- data/lib/operandi/rubocop/cop/operandi/redundant_optional.rb +103 -0
- data/lib/operandi/rubocop/cop/operandi/reserved_name.rb +56 -0
- data/lib/operandi/rubocop/cop/operandi/step_method_exists.rb +134 -0
- data/lib/operandi/rubocop.rb +22 -0
- data/lib/operandi/settings/field.rb +147 -0
- data/lib/operandi/settings/step.rb +105 -0
- data/lib/operandi/utils.rb +36 -0
- data/lib/operandi/version.rb +5 -0
- data/lib/operandi.rb +11 -0
- data/lib/ruby_lsp/operandi/addon.rb +37 -0
- data/lib/ruby_lsp/operandi/definition.rb +134 -0
- data/lib/ruby_lsp/operandi/indexing_enhancement.rb +224 -0
- data/lib/tapioca/dsl/compilers/operandi.rb +378 -0
- data/operandi.gemspec +33 -0
- data/rbi/operandi.rbi +197 -0
- data/sorbet/cache/data.mdb +0 -0
- data/sorbet/cache/lock.mdb +0 -0
- metadata +151 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Operandi
|
|
4
|
+
module RSpec
|
|
5
|
+
module Matchers
|
|
6
|
+
# Matcher for testing output definitions on a service class
|
|
7
|
+
#
|
|
8
|
+
# @example Basic usage
|
|
9
|
+
# expect(MyService).to define_output(:result)
|
|
10
|
+
#
|
|
11
|
+
# @example With type constraint
|
|
12
|
+
# expect(MyService).to define_output(:product).with_type(Product)
|
|
13
|
+
#
|
|
14
|
+
# @example With optional flag
|
|
15
|
+
# expect(MyService).to define_output(:message).optional
|
|
16
|
+
#
|
|
17
|
+
# @example With default value
|
|
18
|
+
# expect(MyService).to define_output(:count).with_default(0)
|
|
19
|
+
#
|
|
20
|
+
# @example Combined
|
|
21
|
+
# expect(MyService).to define_output(:data).with_type(Hash).optional.with_default({})
|
|
22
|
+
def define_output(name)
|
|
23
|
+
DefineOutputMatcher.new(name)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class DefineOutputMatcher
|
|
27
|
+
def initialize(name)
|
|
28
|
+
@name = name
|
|
29
|
+
@expected_type = nil
|
|
30
|
+
@expected_optional = nil
|
|
31
|
+
@expected_default = nil
|
|
32
|
+
@check_default = false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def with_type(type)
|
|
36
|
+
@expected_type = type
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def optional(value = true)
|
|
41
|
+
@expected_optional = value
|
|
42
|
+
self
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def required
|
|
46
|
+
@expected_optional = false
|
|
47
|
+
self
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def with_default(default)
|
|
51
|
+
@check_default = true
|
|
52
|
+
@expected_default = default
|
|
53
|
+
self
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def matches?(service_class)
|
|
57
|
+
@service_class = service_class
|
|
58
|
+
@actual_class = service_class.is_a?(Class) ? service_class : service_class.class
|
|
59
|
+
|
|
60
|
+
return false unless output_defined?
|
|
61
|
+
return false unless type_matches?
|
|
62
|
+
return false unless optional_matches?
|
|
63
|
+
return false unless default_matches?
|
|
64
|
+
|
|
65
|
+
true
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def failure_message
|
|
69
|
+
return "expected #{@actual_class} to define output :#{@name}" unless output_defined?
|
|
70
|
+
return type_failure_message unless type_matches?
|
|
71
|
+
return optional_failure_message unless optional_matches?
|
|
72
|
+
return default_failure_message unless default_matches?
|
|
73
|
+
|
|
74
|
+
""
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def failure_message_when_negated
|
|
78
|
+
"expected #{@actual_class} not to define output :#{@name}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def description
|
|
82
|
+
desc = "define output :#{@name}"
|
|
83
|
+
desc += " with type #{@expected_type}" if @expected_type
|
|
84
|
+
desc += " as optional" if @expected_optional == true
|
|
85
|
+
desc += " as required" if @expected_optional == false
|
|
86
|
+
desc += " with default #{@expected_default.inspect}" if @check_default
|
|
87
|
+
desc
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def output_defined?
|
|
93
|
+
@actual_class.respond_to?(:outputs) && @actual_class.outputs.key?(@name)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def output
|
|
97
|
+
@output ||= @actual_class.outputs[@name]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def type_matches?
|
|
101
|
+
return true if @expected_type.nil?
|
|
102
|
+
|
|
103
|
+
actual_type = output.instance_variable_get(:@type)
|
|
104
|
+
actual_type == @expected_type
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def optional_matches?
|
|
108
|
+
return true if @expected_optional.nil?
|
|
109
|
+
|
|
110
|
+
output.optional == @expected_optional
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def default_matches?
|
|
114
|
+
return true unless @check_default
|
|
115
|
+
|
|
116
|
+
output.default_exists && output.default == @expected_default
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def type_failure_message
|
|
120
|
+
actual_type = output.instance_variable_get(:@type)
|
|
121
|
+
"expected #{@actual_class} output :#{@name} to have type #{@expected_type}, " \
|
|
122
|
+
"but it has type #{actual_type.inspect}"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def optional_failure_message
|
|
126
|
+
if @expected_optional
|
|
127
|
+
"expected #{@actual_class} output :#{@name} to be optional, but it is required"
|
|
128
|
+
else
|
|
129
|
+
"expected #{@actual_class} output :#{@name} to be required, but it is optional"
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def default_failure_message
|
|
134
|
+
if output.default_exists
|
|
135
|
+
"expected #{@actual_class} output :#{@name} to have default #{@expected_default.inspect}, " \
|
|
136
|
+
"but it has default #{output.default.inspect}"
|
|
137
|
+
else
|
|
138
|
+
"expected #{@actual_class} output :#{@name} to have default #{@expected_default.inspect}, " \
|
|
139
|
+
"but no default is defined"
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Operandi
|
|
4
|
+
module RSpec
|
|
5
|
+
module Matchers
|
|
6
|
+
# Matcher for testing step definitions on a service class
|
|
7
|
+
#
|
|
8
|
+
# @example Basic usage
|
|
9
|
+
# expect(MyService).to define_step(:validate)
|
|
10
|
+
#
|
|
11
|
+
# @example With always flag
|
|
12
|
+
# expect(MyService).to define_step(:cleanup).with_always(true)
|
|
13
|
+
#
|
|
14
|
+
# @example With if condition
|
|
15
|
+
# expect(MyService).to define_step(:notify).with_if(:should_notify?)
|
|
16
|
+
#
|
|
17
|
+
# @example With unless condition
|
|
18
|
+
# expect(MyService).to define_step(:skip_validation).with_unless(:production?)
|
|
19
|
+
#
|
|
20
|
+
# @example Check multiple steps
|
|
21
|
+
# expect(MyService).to define_steps(:validate, :process, :save)
|
|
22
|
+
#
|
|
23
|
+
# @example Check step order
|
|
24
|
+
# expect(MyService).to define_steps_in_order(:validate, :process, :save)
|
|
25
|
+
def define_step(name)
|
|
26
|
+
DefineStepMatcher.new(name)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def define_steps(*names)
|
|
30
|
+
DefineStepsMatcher.new(names, ordered: false)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def define_steps_in_order(*names)
|
|
34
|
+
DefineStepsMatcher.new(names, ordered: true)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class DefineStepMatcher
|
|
38
|
+
def initialize(name)
|
|
39
|
+
@name = name
|
|
40
|
+
@expected_always = nil
|
|
41
|
+
@expected_if = nil
|
|
42
|
+
@expected_unless = nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def with_always(value = true)
|
|
46
|
+
@expected_always = value
|
|
47
|
+
self
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def with_if(condition)
|
|
51
|
+
@expected_if = condition
|
|
52
|
+
self
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def with_unless(condition)
|
|
56
|
+
@expected_unless = condition
|
|
57
|
+
self
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def matches?(service_class)
|
|
61
|
+
@service_class = service_class
|
|
62
|
+
@actual_class = service_class.is_a?(Class) ? service_class : service_class.class
|
|
63
|
+
|
|
64
|
+
return false unless step_defined?
|
|
65
|
+
return false unless always_matches?
|
|
66
|
+
return false unless if_matches?
|
|
67
|
+
return false unless unless_matches?
|
|
68
|
+
|
|
69
|
+
true
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def failure_message
|
|
73
|
+
return "expected #{@actual_class} to define step :#{@name}" unless step_defined?
|
|
74
|
+
return always_failure_message unless always_matches?
|
|
75
|
+
return if_failure_message unless if_matches?
|
|
76
|
+
return unless_failure_message unless unless_matches?
|
|
77
|
+
|
|
78
|
+
""
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def failure_message_when_negated
|
|
82
|
+
"expected #{@actual_class} not to define step :#{@name}"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def description
|
|
86
|
+
desc = "define step :#{@name}"
|
|
87
|
+
desc += " with always: #{@expected_always}" unless @expected_always.nil?
|
|
88
|
+
desc += " with if: #{@expected_if.inspect}" if @expected_if
|
|
89
|
+
desc += " with unless: #{@expected_unless.inspect}" if @expected_unless
|
|
90
|
+
desc
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def step_defined?
|
|
96
|
+
@actual_class.respond_to?(:steps) && @actual_class.steps.key?(@name)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def step
|
|
100
|
+
@step ||= @actual_class.steps[@name]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def always_matches?
|
|
104
|
+
return true if @expected_always.nil?
|
|
105
|
+
|
|
106
|
+
step.always == @expected_always
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def if_matches?
|
|
110
|
+
return true if @expected_if.nil?
|
|
111
|
+
|
|
112
|
+
actual_if = step.instance_variable_get(:@if)
|
|
113
|
+
actual_if == @expected_if
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def unless_matches?
|
|
117
|
+
return true if @expected_unless.nil?
|
|
118
|
+
|
|
119
|
+
actual_unless = step.instance_variable_get(:@unless)
|
|
120
|
+
actual_unless == @expected_unless
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def always_failure_message
|
|
124
|
+
"expected #{@actual_class} step :#{@name} to have always: #{@expected_always}, " \
|
|
125
|
+
"but it has always: #{step.always.inspect}"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def if_failure_message
|
|
129
|
+
actual_if = step.instance_variable_get(:@if)
|
|
130
|
+
"expected #{@actual_class} step :#{@name} to have if: #{@expected_if.inspect}, " \
|
|
131
|
+
"but it has if: #{actual_if.inspect}"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def unless_failure_message
|
|
135
|
+
actual_unless = step.instance_variable_get(:@unless)
|
|
136
|
+
"expected #{@actual_class} step :#{@name} to have unless: #{@expected_unless.inspect}, " \
|
|
137
|
+
"but it has unless: #{actual_unless.inspect}"
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
class DefineStepsMatcher
|
|
142
|
+
def initialize(names, ordered:)
|
|
143
|
+
@names = names
|
|
144
|
+
@ordered = ordered
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def matches?(service_class)
|
|
148
|
+
@service_class = service_class
|
|
149
|
+
@actual_class = service_class.is_a?(Class) ? service_class : service_class.class
|
|
150
|
+
@missing_steps = []
|
|
151
|
+
@actual_order = []
|
|
152
|
+
|
|
153
|
+
return false unless all_steps_defined?
|
|
154
|
+
return false unless order_matches?
|
|
155
|
+
|
|
156
|
+
true
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def failure_message
|
|
160
|
+
return missing_steps_failure_message unless all_steps_defined?
|
|
161
|
+
return order_failure_message unless order_matches?
|
|
162
|
+
|
|
163
|
+
""
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def failure_message_when_negated
|
|
167
|
+
if @ordered
|
|
168
|
+
"expected #{@actual_class} not to define steps #{@names.inspect} in that order"
|
|
169
|
+
else
|
|
170
|
+
"expected #{@actual_class} not to define steps #{@names.inspect}"
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def description
|
|
175
|
+
if @ordered
|
|
176
|
+
"define steps #{@names.inspect} in order"
|
|
177
|
+
else
|
|
178
|
+
"define steps #{@names.inspect}"
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
private
|
|
183
|
+
|
|
184
|
+
def all_steps_defined?
|
|
185
|
+
return false unless @actual_class.respond_to?(:steps)
|
|
186
|
+
|
|
187
|
+
actual_step_names = @actual_class.steps.keys
|
|
188
|
+
@missing_steps = @names - actual_step_names
|
|
189
|
+
@missing_steps.empty?
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def order_matches?
|
|
193
|
+
return true unless @ordered
|
|
194
|
+
|
|
195
|
+
actual_step_names = @actual_class.steps.keys
|
|
196
|
+
@actual_order = @names.select { |name| actual_step_names.include?(name) }
|
|
197
|
+
|
|
198
|
+
# Check if the expected steps appear in the same order in actual steps
|
|
199
|
+
last_index = -1
|
|
200
|
+
@names.all? do |name|
|
|
201
|
+
current_index = actual_step_names.index(name)
|
|
202
|
+
return false unless current_index
|
|
203
|
+
return false unless current_index > last_index
|
|
204
|
+
|
|
205
|
+
last_index = current_index
|
|
206
|
+
true
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def missing_steps_failure_message
|
|
211
|
+
"expected #{@actual_class} to define steps #{@names.inspect}, " \
|
|
212
|
+
"but missing: #{@missing_steps.inspect}"
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def order_failure_message
|
|
216
|
+
actual_step_names = @actual_class.steps.keys
|
|
217
|
+
"expected #{@actual_class} to define steps #{@names.inspect} in that order, " \
|
|
218
|
+
"but actual order is: #{actual_step_names.inspect}"
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Operandi
|
|
4
|
+
module RSpec
|
|
5
|
+
module Matchers
|
|
6
|
+
# Matcher for testing step execution on a service instance
|
|
7
|
+
# NOTE: This matcher requires the service to track executed steps.
|
|
8
|
+
# Add a callback in your service to track execution:
|
|
9
|
+
#
|
|
10
|
+
# after_step_run do |service, step_name|
|
|
11
|
+
# service.executed_steps << step_name
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# @example Basic usage (requires executed_steps tracking)
|
|
15
|
+
# expect(service).to execute_step(:validate)
|
|
16
|
+
#
|
|
17
|
+
# @example Check step was skipped
|
|
18
|
+
# expect(service).to skip_step(:notify)
|
|
19
|
+
#
|
|
20
|
+
# @example Check multiple steps executed
|
|
21
|
+
# expect(service).to execute_steps(:validate, :process, :save)
|
|
22
|
+
#
|
|
23
|
+
# @example Check execution order
|
|
24
|
+
# expect(service).to execute_steps_in_order(:validate, :process, :save)
|
|
25
|
+
def execute_step(name)
|
|
26
|
+
ExecuteStepMatcher.new(name)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def skip_step(name)
|
|
30
|
+
SkipStepMatcher.new(name)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def execute_steps(*names)
|
|
34
|
+
ExecuteStepsMatcher.new(names, ordered: false)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def execute_steps_in_order(*names)
|
|
38
|
+
ExecuteStepsMatcher.new(names, ordered: true)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
class ExecuteStepMatcher
|
|
42
|
+
def initialize(name)
|
|
43
|
+
@name = name
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def matches?(service)
|
|
47
|
+
@service = service
|
|
48
|
+
|
|
49
|
+
return false unless service_tracks_steps?
|
|
50
|
+
return false unless step_executed?
|
|
51
|
+
|
|
52
|
+
true
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def failure_message
|
|
56
|
+
return tracking_not_available_message unless service_tracks_steps?
|
|
57
|
+
|
|
58
|
+
"expected service to execute step :#{@name}, " \
|
|
59
|
+
"but executed steps were: #{executed_steps.inspect}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def failure_message_when_negated
|
|
63
|
+
"expected service not to execute step :#{@name}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def description
|
|
67
|
+
"execute step :#{@name}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def service_tracks_steps?
|
|
73
|
+
@service.respond_to?(:executed_steps)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def executed_steps
|
|
77
|
+
@service.executed_steps
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def step_executed?
|
|
81
|
+
executed_steps.include?(@name)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def tracking_not_available_message
|
|
85
|
+
"cannot verify step execution because service does not track executed steps. " \
|
|
86
|
+
"Add `after_step_run { |service, step| service.executed_steps << step }` to your service."
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
class SkipStepMatcher
|
|
91
|
+
def initialize(name)
|
|
92
|
+
@name = name
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def matches?(service)
|
|
96
|
+
@service = service
|
|
97
|
+
|
|
98
|
+
return false unless service_tracks_steps?
|
|
99
|
+
return false unless step_skipped?
|
|
100
|
+
|
|
101
|
+
true
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def failure_message
|
|
105
|
+
return tracking_not_available_message unless service_tracks_steps?
|
|
106
|
+
|
|
107
|
+
"expected service to skip step :#{@name}, but it was executed. " \
|
|
108
|
+
"Executed steps: #{executed_steps.inspect}"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def failure_message_when_negated
|
|
112
|
+
"expected service not to skip step :#{@name} (expected it to execute)"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def description
|
|
116
|
+
"skip step :#{@name}"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
private
|
|
120
|
+
|
|
121
|
+
def service_tracks_steps?
|
|
122
|
+
@service.respond_to?(:executed_steps)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def executed_steps
|
|
126
|
+
@service.executed_steps
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def step_skipped?
|
|
130
|
+
!executed_steps.include?(@name)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def tracking_not_available_message
|
|
134
|
+
"cannot verify step execution because service does not track executed steps. " \
|
|
135
|
+
"Add `after_step_run { |service, step| service.executed_steps << step }` to your service."
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
class ExecuteStepsMatcher
|
|
140
|
+
def initialize(names, ordered:)
|
|
141
|
+
@names = names
|
|
142
|
+
@ordered = ordered
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def matches?(service)
|
|
146
|
+
@service = service
|
|
147
|
+
@missing_steps = []
|
|
148
|
+
|
|
149
|
+
return false unless service_tracks_steps?
|
|
150
|
+
return false unless all_steps_executed?
|
|
151
|
+
return false unless order_matches?
|
|
152
|
+
|
|
153
|
+
true
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def failure_message
|
|
157
|
+
return tracking_not_available_message unless service_tracks_steps?
|
|
158
|
+
return missing_steps_failure_message unless all_steps_executed?
|
|
159
|
+
return order_failure_message unless order_matches?
|
|
160
|
+
|
|
161
|
+
""
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def failure_message_when_negated
|
|
165
|
+
if @ordered
|
|
166
|
+
"expected service not to execute steps #{@names.inspect} in that order"
|
|
167
|
+
else
|
|
168
|
+
"expected service not to execute steps #{@names.inspect}"
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def description
|
|
173
|
+
if @ordered
|
|
174
|
+
"execute steps #{@names.inspect} in order"
|
|
175
|
+
else
|
|
176
|
+
"execute steps #{@names.inspect}"
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
private
|
|
181
|
+
|
|
182
|
+
def service_tracks_steps?
|
|
183
|
+
@service.respond_to?(:executed_steps)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def executed_steps
|
|
187
|
+
@service.executed_steps
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def all_steps_executed?
|
|
191
|
+
@missing_steps = @names.reject { |name| executed_steps.include?(name) }
|
|
192
|
+
@missing_steps.empty?
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def order_matches?
|
|
196
|
+
return true unless @ordered
|
|
197
|
+
|
|
198
|
+
# Check if the expected steps appear in the same order in executed steps
|
|
199
|
+
last_index = -1
|
|
200
|
+
@names.all? do |name|
|
|
201
|
+
current_index = executed_steps.index(name)
|
|
202
|
+
return false unless current_index
|
|
203
|
+
return false unless current_index > last_index
|
|
204
|
+
|
|
205
|
+
last_index = current_index
|
|
206
|
+
true
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def missing_steps_failure_message
|
|
211
|
+
"expected service to execute steps #{@names.inspect}, " \
|
|
212
|
+
"but missing: #{@missing_steps.inspect}. " \
|
|
213
|
+
"Executed steps: #{executed_steps.inspect}"
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def order_failure_message
|
|
217
|
+
"expected service to execute steps #{@names.inspect} in that order, " \
|
|
218
|
+
"but actual execution order was: #{executed_steps.inspect}"
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def tracking_not_available_message
|
|
222
|
+
"cannot verify step execution because service does not track executed steps. " \
|
|
223
|
+
"Add `after_step_run { |service, step| service.executed_steps << step }` to your service."
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|