actionable-rb 1.9.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/.claude-plugin/marketplace.json +15 -0
- data/.claude-plugin/plugin.json +9 -0
- data/.rspec +3 -0
- data/AGENTS.md +150 -0
- data/CHANGELOG.md +766 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +555 -0
- data/UPGRADING.md +957 -0
- data/USAGE.md +1291 -0
- data/docs/getting_started.md +611 -0
- data/lib/actionable/action.rb +1220 -0
- data/lib/actionable/batch_result.rb +92 -0
- data/lib/actionable/errors.rb +36 -0
- data/lib/actionable/history.rb +91 -0
- data/lib/actionable/input_dispatch.rb +94 -0
- data/lib/actionable/job.rb +157 -0
- data/lib/actionable/measurement.rb +64 -0
- data/lib/actionable/rails/after_commit.rb +142 -0
- data/lib/actionable/rails/proxy_validator.rb +59 -0
- data/lib/actionable/rails/transactions.rb +95 -0
- data/lib/actionable/rails.rb +24 -0
- data/lib/actionable/railtie.rb +10 -0
- data/lib/actionable/rbs.rb +502 -0
- data/lib/actionable/registry.rb +64 -0
- data/lib/actionable/result.rb +166 -0
- data/lib/actionable/results/failure.rb +43 -0
- data/lib/actionable/results/skipped.rb +30 -0
- data/lib/actionable/results/success.rb +14 -0
- data/lib/actionable/rspec/coverage/report.rb +264 -0
- data/lib/actionable/rspec/coverage/tracker.rb +330 -0
- data/lib/actionable/rspec/coverage.rb +45 -0
- data/lib/actionable/rspec/matchers.rb +266 -0
- data/lib/actionable/rspec/stubs.rb +250 -0
- data/lib/actionable/rspec.rb +40 -0
- data/lib/actionable/rubocop/config.yml +8 -0
- data/lib/actionable/rubocop/cop/actionable/verb_not_last.rb +62 -0
- data/lib/actionable/rubocop/plugin.rb +31 -0
- data/lib/actionable/rubocop.rb +25 -0
- data/lib/actionable/runner.rb +300 -0
- data/lib/actionable/steps/action.rb +76 -0
- data/lib/actionable/steps/base.rb +107 -0
- data/lib/actionable/steps/case.rb +124 -0
- data/lib/actionable/steps/method.rb +39 -0
- data/lib/actionable/steps.rb +27 -0
- data/lib/actionable/value_match.rb +22 -0
- data/lib/actionable/verb_position.rb +574 -0
- data/lib/actionable/version.rb +6 -0
- data/lib/actionable-rb.rb +6 -0
- data/lib/actionable.rb +59 -0
- data/sig/actionable/dsl.rbs +42 -0
- data/sig/actionable/rails.rbs +16 -0
- data/sig/actionable.rbs +2593 -0
- data/skills/actionable/SKILL.md +214 -0
- metadata +132 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Actionable
|
|
4
|
+
module RSpec
|
|
5
|
+
# Helpers mixed into example groups by +actionable/rspec+ that stub an
|
|
6
|
+
# action's +.run+ to return a real {Success} / {Failure} built from a hash,
|
|
7
|
+
# so callers can assert on +result.code+/+output+/+errors+ without executing
|
|
8
|
+
# the action (decision D12).
|
|
9
|
+
#
|
|
10
|
+
# +allow_*+ permits the call (a stub); +stub_*+ also sets a call expectation
|
|
11
|
+
# (a mock, verified on the example) — mirroring RSpec's allow/expect split.
|
|
12
|
+
#
|
|
13
|
+
# A stubbed failure of an action that declares failures with
|
|
14
|
+
# {Actionable::Action.fails} carries the declared metadata (and message,
|
|
15
|
+
# when none is given) for its code, and a code the action doesn't declare
|
|
16
|
+
# raises +ArgumentError+, so a stub can't return what the action never
|
|
17
|
+
# could (decision D20). A stubbed skip follows {Actionable::Action.skips}
|
|
18
|
+
# the same way (decision D21), the default +:skipped+ included.
|
|
19
|
+
module Stubs
|
|
20
|
+
# @param action_class [Class<Actionable::Action>] the action whose +.run+ is stubbed
|
|
21
|
+
# @param opts [Object] {#actionable_success_result}'s keywords (+message:+, +output:+, +errors:+)
|
|
22
|
+
# @return [Success] the stubbed result
|
|
23
|
+
def allow_actionable_success(action_class, **opts)
|
|
24
|
+
allow_run(action_class, actionable_success_result(**opts))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @param action_class [Class<Actionable::Action>] the action whose +.run+ is stubbed
|
|
28
|
+
# @param opts [Object] {#actionable_success_result}'s keywords (+message:+, +output:+, +errors:+)
|
|
29
|
+
# @return [Success] the stubbed result
|
|
30
|
+
def stub_actionable_success(action_class, **opts)
|
|
31
|
+
expect_run(action_class, actionable_success_result(**opts))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @param action_class [Class<Actionable::Action>] the action whose +.run+ is stubbed
|
|
35
|
+
# @param opts [Object] {#actionable_failure_result}'s keywords; +code:+ defaults to +:error+
|
|
36
|
+
# @return [Failure] the stubbed result
|
|
37
|
+
# @raise [ArgumentError] when the action declares failures but not the code
|
|
38
|
+
def allow_actionable_failure(action_class, **opts)
|
|
39
|
+
allow_run(action_class, declared_failure_result(action_class, **opts))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @param action_class [Class<Actionable::Action>] the action whose +.run+ is stubbed
|
|
43
|
+
# @param opts [Object] {#actionable_failure_result}'s keywords; +code:+ defaults to +:error+
|
|
44
|
+
# @return [Failure] the stubbed result
|
|
45
|
+
# @raise [ArgumentError] when the action declares failures but not the code
|
|
46
|
+
def stub_actionable_failure(action_class, **opts)
|
|
47
|
+
expect_run(action_class, declared_failure_result(action_class, **opts))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @param action_class [Class<Actionable::Action>] the action whose +.run+ is stubbed
|
|
51
|
+
# @param opts [Object] {#actionable_skip_result}'s keywords; +code:+ defaults to +:skipped+
|
|
52
|
+
# @return [Skipped] the stubbed result
|
|
53
|
+
# @raise [ArgumentError] when the action declares skips but not the code
|
|
54
|
+
def allow_actionable_skip(action_class, **opts)
|
|
55
|
+
allow_run(action_class, declared_skip_result(action_class, **opts))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @param action_class [Class<Actionable::Action>] the action whose +.run+ is stubbed
|
|
59
|
+
# @param opts [Object] {#actionable_skip_result}'s keywords; +code:+ defaults to +:skipped+
|
|
60
|
+
# @return [Skipped] the stubbed result
|
|
61
|
+
# @raise [ArgumentError] when the action declares skips but not the code
|
|
62
|
+
def stub_actionable_skip(action_class, **opts)
|
|
63
|
+
expect_run(action_class, declared_skip_result(action_class, **opts))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Build a real {Success} from a hash (no stubbing).
|
|
67
|
+
#
|
|
68
|
+
# @param message [String, nil]
|
|
69
|
+
# @param output [Hash{Symbol=>Object}, FieldStruct::Base]
|
|
70
|
+
# @param errors [Hash{Symbol=>String, Array<String>}]
|
|
71
|
+
# @return [Success]
|
|
72
|
+
def actionable_success_result(message: nil, output: {}, errors: {})
|
|
73
|
+
apply_errors(Actionable::Success.new(message: message, output: output), errors)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Build a real {Failure} from a hash (no stubbing).
|
|
77
|
+
#
|
|
78
|
+
# @param code [Symbol]
|
|
79
|
+
# @param message [String, nil]
|
|
80
|
+
# @param output [Hash{Symbol=>Object}, FieldStruct::Base]
|
|
81
|
+
# @param errors [Hash{Symbol=>String, Array<String>}]
|
|
82
|
+
# @param metadata [Hash{Symbol=>Object}] the failure's per-code metadata
|
|
83
|
+
# @return [Failure]
|
|
84
|
+
def actionable_failure_result(code: :error, message: nil, output: {}, errors: {}, metadata: {})
|
|
85
|
+
failure = Actionable::Failure.new(code: code, message: message, output: output, metadata: metadata)
|
|
86
|
+
apply_errors(failure, errors)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Build a real {Skipped} from a hash (no stubbing).
|
|
90
|
+
#
|
|
91
|
+
# @param code [Symbol]
|
|
92
|
+
# @param message [String, nil]
|
|
93
|
+
# @param output [Hash{Symbol=>Object}, FieldStruct::Base]
|
|
94
|
+
# @param metadata [Hash{Symbol=>Object}] the skip's per-code metadata
|
|
95
|
+
# @return [Skipped]
|
|
96
|
+
def actionable_skip_result(code: :skipped, message: nil, output: {}, metadata: {})
|
|
97
|
+
Actionable::Skipped.new(code: code, message: message, output: output, metadata: metadata)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
# A {Failure} for +action_class+: the declared metadata and message for
|
|
103
|
+
# its code, or a refusal when the action declares failures but not it.
|
|
104
|
+
#
|
|
105
|
+
# @param action_class [Class<Actionable::Action>]
|
|
106
|
+
# @param code [Symbol, String]
|
|
107
|
+
# @param opts [Object] the rest of {#actionable_failure_result}'s keywords
|
|
108
|
+
# @raise [ArgumentError] when the action declares failures but not +code+
|
|
109
|
+
# @return [Failure]
|
|
110
|
+
def declared_failure_result(action_class, code: :error, **opts)
|
|
111
|
+
unless action_class.allows_failure_code?(code)
|
|
112
|
+
raise ArgumentError, Actionable::RSpec.undeclared_code_message(action_class, code)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
declaration = action_class.failure_declarations.fetch(code.to_sym, {message: nil, metadata: {}})
|
|
116
|
+
opts[:message] ||= declaration[:message]
|
|
117
|
+
actionable_failure_result(code: code, metadata: declaration[:metadata], **opts)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# A {Skipped} for +action_class+: the declared metadata and message for
|
|
121
|
+
# its code, or a refusal when the action declares skips but not it.
|
|
122
|
+
#
|
|
123
|
+
# @param action_class [Class<Actionable::Action>]
|
|
124
|
+
# @param code [Symbol, String]
|
|
125
|
+
# @param opts [Object] the rest of {#actionable_skip_result}'s keywords
|
|
126
|
+
# @raise [ArgumentError] when the action declares skips but not +code+
|
|
127
|
+
# @return [Skipped]
|
|
128
|
+
def declared_skip_result(action_class, code: :skipped, **opts)
|
|
129
|
+
unless action_class.allows_skip_code?(code)
|
|
130
|
+
raise ArgumentError, Actionable::RSpec.undeclared_code_message(action_class, code, outcome: :skip)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
declaration = action_class.skip_declarations.fetch(code.to_sym, {message: nil, metadata: {}})
|
|
134
|
+
opts[:message] ||= declaration[:message]
|
|
135
|
+
actionable_skip_result(code: code, metadata: declaration[:metadata], **opts)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Permit +action_class.run+, answering +result+ as a real run would.
|
|
139
|
+
#
|
|
140
|
+
# @param action_class [Class<Actionable::Action>]
|
|
141
|
+
# @param result [Result]
|
|
142
|
+
# @return [Result] the result the stub answers
|
|
143
|
+
def allow_run(action_class, result)
|
|
144
|
+
result = as_action_result(action_class, result)
|
|
145
|
+
allow(action_class).to receive(:run) { stubbed_run(action_class, result) }
|
|
146
|
+
result
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Expect +action_class.run+ (verified on the example), answering +result+
|
|
150
|
+
# as a real run would.
|
|
151
|
+
#
|
|
152
|
+
# @param action_class [Class<Actionable::Action>]
|
|
153
|
+
# @param result [Result]
|
|
154
|
+
# @return [Result] the result the stub answers
|
|
155
|
+
def expect_run(action_class, result)
|
|
156
|
+
result = as_action_result(action_class, result)
|
|
157
|
+
expect(action_class).to receive(:run) { stubbed_run(action_class, result) }
|
|
158
|
+
result
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# The stubbed result as a real run would return it (decision D23): its
|
|
162
|
+
# output shaped by the schema it is held to, then as the action's own
|
|
163
|
+
# result class when it has typed outcomes.
|
|
164
|
+
#
|
|
165
|
+
# @param action_class [Class<Actionable::Action>]
|
|
166
|
+
# @param result [Result]
|
|
167
|
+
# @return [Result]
|
|
168
|
+
def as_action_result(action_class, result)
|
|
169
|
+
shape_output(action_class, result)
|
|
170
|
+
action_class.__send__(:typed_result, result)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Mirror the runner: output for a code with a schema becomes that schema
|
|
174
|
+
# (refused when it breaks it, since a real run would end as
|
|
175
|
+
# +invalid_output+ instead), and under typed outcomes a code without one
|
|
176
|
+
# carries an empty output (refused when output is given, as a real run
|
|
177
|
+
# refuses it). The gem's own failure codes keep what they are given.
|
|
178
|
+
#
|
|
179
|
+
# @param action_class [Class<Actionable::Action>]
|
|
180
|
+
# @param result [Result] updated in place
|
|
181
|
+
# @raise [ArgumentError] when the output breaks what a real run allows
|
|
182
|
+
# @return [void]
|
|
183
|
+
def shape_output(action_class, result)
|
|
184
|
+
schema = stubbed_output_schema(action_class, result)
|
|
185
|
+
return result.output = build_stubbed_output(action_class, result, schema) if schema
|
|
186
|
+
return unless action_class.typed_outcomes? && !result.success?
|
|
187
|
+
return if result.failure? && Actionable::Action::GEM_FAILURE_CODES.include?(result.code)
|
|
188
|
+
|
|
189
|
+
unless result.output.respond_to?(:empty?) && result.output.empty?
|
|
190
|
+
raise ArgumentError, "#{action_class.name || "action"} output for #{result.code.inspect}, which declares no " \
|
|
191
|
+
'output schema; a real run refuses it'
|
|
192
|
+
end
|
|
193
|
+
result.output = {}
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# @return [Class<FieldStruct::Base>, nil] the schema the result's output
|
|
197
|
+
# is held to: its code's, or the success schema under typed outcomes
|
|
198
|
+
# @param action_class [Class<Actionable::Action>]
|
|
199
|
+
# @param result [Result]
|
|
200
|
+
def stubbed_output_schema(action_class, result)
|
|
201
|
+
return action_class.skip_output_schema(result.code) if result.skipped?
|
|
202
|
+
return action_class.failure_output_schema(result.code) if result.failure?
|
|
203
|
+
|
|
204
|
+
action_class.output_schema if action_class.typed_outcomes?
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# The stub's output as an instance of +schema+, built from the fields of
|
|
208
|
+
# +result+'s output that the schema declares.
|
|
209
|
+
#
|
|
210
|
+
# @param action_class [Class<Actionable::Action>]
|
|
211
|
+
# @param result [Result]
|
|
212
|
+
# @param schema [Class<FieldStruct::Base>]
|
|
213
|
+
# @raise [ArgumentError] when the output breaks +schema+
|
|
214
|
+
# @return [FieldStruct::Base]
|
|
215
|
+
def build_stubbed_output(action_class, result, schema)
|
|
216
|
+
given = result.output.is_a?(FieldStruct::Base) ? result.output.attributes : result.output.to_h
|
|
217
|
+
given = given.slice(*schema.attribute_names)
|
|
218
|
+
# A parent absorbs only the fields a run set, so a stub sets the ones it was given.
|
|
219
|
+
result.output_fields_set = given.keys
|
|
220
|
+
output = schema.new(**given)
|
|
221
|
+
return output if output.valid?
|
|
222
|
+
|
|
223
|
+
raise ArgumentError, "#{action_class.name || "action"} output for #{result.code.inspect} breaks its schema " \
|
|
224
|
+
"(#{output.errors.full_messages.join(", ")}); a real run would end as invalid_output"
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# The stub's response: +result+, noted as a stubbed run when outcome
|
|
228
|
+
# coverage is loaded (decision D22), so a report can tell an outcome
|
|
229
|
+
# only ever stubbed from one never reached at all.
|
|
230
|
+
#
|
|
231
|
+
# @param action_class [Class<Actionable::Action>]
|
|
232
|
+
# @param result [Result]
|
|
233
|
+
# @return [Result] +result+
|
|
234
|
+
def stubbed_run(action_class, result)
|
|
235
|
+
Actionable::Coverage.record_stub(action_class, result) if defined?(Actionable::Coverage)
|
|
236
|
+
result
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Add +errors+ to +result+'s errors.
|
|
240
|
+
#
|
|
241
|
+
# @param result [Result]
|
|
242
|
+
# @param errors [Hash{Symbol=>String, Array<String>}]
|
|
243
|
+
# @return [Result] +result+
|
|
244
|
+
def apply_errors(result, errors)
|
|
245
|
+
errors.each { |field, messages| Array(messages).each { |message| result.errors.add(field, message) } }
|
|
246
|
+
result
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Optional RSpec integration (decision D12). Load it explicitly with
|
|
4
|
+
# `require 'actionable/rspec'` (typically in spec_helper, after rspec). Never
|
|
5
|
+
# loaded by the core entry point.
|
|
6
|
+
require_relative '../actionable' # ensure the core is loaded (self-contained entry)
|
|
7
|
+
require_relative 'rspec/matchers'
|
|
8
|
+
require_relative 'rspec/stubs'
|
|
9
|
+
|
|
10
|
+
module Actionable
|
|
11
|
+
# RSpec matchers and stubs for actions (decision D12).
|
|
12
|
+
module RSpec
|
|
13
|
+
# The refusal for a stub, +and_fail+ or +and_skip+ naming a code an action
|
|
14
|
+
# that declares failures (decision D20) or skips (decision D21) doesn't
|
|
15
|
+
# declare.
|
|
16
|
+
#
|
|
17
|
+
# @param action_class [Class<Actionable::Action>]
|
|
18
|
+
# @param code [Symbol, String]
|
|
19
|
+
# @param outcome [:failure, :skip] which declarations the code was checked against
|
|
20
|
+
# @return [String]
|
|
21
|
+
def self.undeclared_code_message(action_class, code, outcome: :failure)
|
|
22
|
+
if outcome == :skip
|
|
23
|
+
declared = action_class.declared_skips.keys.map(&:inspect).join(', ')
|
|
24
|
+
return "#{action_class.name || "action"} does not declare skip #{code.to_sym.inspect} " \
|
|
25
|
+
"(declared: #{declared}); a stub or and_skip with it tests a skip the action can never return"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
declared = action_class.declared_failures.keys.map(&:inspect).join(', ')
|
|
29
|
+
"#{action_class.name || "action"} does not declare #{code.to_sym.inspect} (declared: #{declared}); " \
|
|
30
|
+
'a stub or and_fail with it tests a failure the action can never return'
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
if defined?(RSpec)
|
|
36
|
+
RSpec.configure do |config|
|
|
37
|
+
config.include Actionable::RSpec::Matchers
|
|
38
|
+
config.include Actionable::RSpec::Stubs
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Actionable/VerbNotLast:
|
|
2
|
+
Description: 'A plain fail/fail_with/skip/succeed must be the last expression of its step or hook method.'
|
|
3
|
+
Enabled: true
|
|
4
|
+
# Subclasses of these (or of a class in the same file that reaches one) are
|
|
5
|
+
# checked; any other class, e.g. a dry-transaction with its own `step`, is not.
|
|
6
|
+
ActionBaseClasses:
|
|
7
|
+
- Actionable::Action
|
|
8
|
+
- ApplicationAction
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Actionable
|
|
6
|
+
# A plain recording verb (+fail+ / +fail_with+ / +skip+ / +succeed+) must
|
|
7
|
+
# be the last expression of its step or hook method: it records the
|
|
8
|
+
# outcome and keeps running, so code after it still runs (owner rulings
|
|
9
|
+
# R5, R10). The
|
|
10
|
+
# same rules as the gem's first-run check, from the same analyzer
|
|
11
|
+
# (+Actionable::VerbPosition+); USAGE (Control flow) has the table.
|
|
12
|
+
#
|
|
13
|
+
# It sees one file: a class whose superclass is an action base (see
|
|
14
|
+
# +ActionBaseClasses+) and that declares +step+ / +case_step+, and the
|
|
15
|
+
# methods that class names in a step, case-step branch or hook
|
|
16
|
+
# declaration. A class with another +step+ DSL (dry-transaction) is
|
|
17
|
+
# left alone.
|
|
18
|
+
#
|
|
19
|
+
# +ActionBaseClasses+ (default +Actionable::Action+, +ApplicationAction+)
|
|
20
|
+
# lists the base classes by full name; a class defined in the same file
|
|
21
|
+
# whose superclass chain reaches one counts too. A superclass name
|
|
22
|
+
# resolves lexically, as Ruby looks it up, so two same-named classes
|
|
23
|
+
# in different modules are told apart.
|
|
24
|
+
#
|
|
25
|
+
# @example
|
|
26
|
+
# # bad
|
|
27
|
+
# def charge
|
|
28
|
+
# fail :card_expired if card.expired?
|
|
29
|
+
# notify
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
# # good
|
|
33
|
+
# def charge
|
|
34
|
+
# fail! :card_expired if card.expired?
|
|
35
|
+
# notify
|
|
36
|
+
# end
|
|
37
|
+
#
|
|
38
|
+
# # good
|
|
39
|
+
# def charge
|
|
40
|
+
# notify
|
|
41
|
+
# fail :card_expired if card.expired?
|
|
42
|
+
# end
|
|
43
|
+
class VerbNotLast < Base
|
|
44
|
+
# @return [String]
|
|
45
|
+
MSG = '`%<verb>s` records the outcome and keeps running, and more code runs after it: ' \
|
|
46
|
+
"use `%<verb>s!` to stop here, or make it the method's last expression."
|
|
47
|
+
|
|
48
|
+
# @return [void]
|
|
49
|
+
def on_new_investigation
|
|
50
|
+
result = ::Prism.parse(processed_source.raw_source)
|
|
51
|
+
return unless result.errors.empty?
|
|
52
|
+
|
|
53
|
+
base_classes = Array(cop_config.fetch('ActionBaseClasses', ::Actionable::VerbPosition::ACTION_BASE_CLASSES))
|
|
54
|
+
::Actionable::VerbPosition.declared_step_offenses(result.value, base_classes: base_classes).each do |offense|
|
|
55
|
+
range = Parser::Source::Range.new(processed_source.buffer, offense.start_character, offense.end_character)
|
|
56
|
+
add_offense(range, message: format(MSG, verb: offense.verb))
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pathname'
|
|
4
|
+
|
|
5
|
+
module Actionable
|
|
6
|
+
# The lint_roller plugin RuboCop >= 1.72 loads for
|
|
7
|
+
# +plugins: [{actionable-rb: {require_path: actionable/rubocop}}]+ (the gemspec
|
|
8
|
+
# names it in +default_lint_roller_plugin+). It enables
|
|
9
|
+
# +Actionable/VerbNotLast+ through +config.yml+ beside this file.
|
|
10
|
+
class RubocopPlugin < LintRoller::Plugin
|
|
11
|
+
# @return [LintRoller::About]
|
|
12
|
+
def about
|
|
13
|
+
LintRoller::About.new(
|
|
14
|
+
name: 'actionable-rb', version: VERSION, homepage: 'https://github.com/Paymentbox-com/actionable',
|
|
15
|
+
description: 'Actionable/VerbNotLast: a plain fail/fail_with/skip/succeed must end its step or hook method.'
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @param context [LintRoller::Context]
|
|
20
|
+
# @return [Boolean]
|
|
21
|
+
def supported?(context)
|
|
22
|
+
context.engine == :rubocop
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @param _context [LintRoller::Context]
|
|
26
|
+
# @return [LintRoller::Rules]
|
|
27
|
+
def rules(_context)
|
|
28
|
+
LintRoller::Rules.new(type: :path, config_format: :rubocop, value: Pathname.new(__dir__).join('config.yml'))
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The opt-in RuboCop integration: the +Actionable/VerbNotLast+ cop (owner
|
|
4
|
+
# ruling R5). Never loaded by +require 'actionable'+; RuboCop loads it from
|
|
5
|
+
# a project's +.rubocop.yml+:
|
|
6
|
+
#
|
|
7
|
+
# plugins:
|
|
8
|
+
# - actionable-rb:
|
|
9
|
+
# require_path: actionable/rubocop
|
|
10
|
+
#
|
|
11
|
+
# or, on RuboCop < 1.72, +require: [actionable/rubocop]+. RuboCop depends on
|
|
12
|
+
# Prism, so the cop always has it. Loads only the rule analyzer, not the gem.
|
|
13
|
+
require 'rubocop'
|
|
14
|
+
require 'prism'
|
|
15
|
+
require_relative 'version'
|
|
16
|
+
require_relative 'verb_position'
|
|
17
|
+
require_relative 'rubocop/cop/actionable/verb_not_last'
|
|
18
|
+
|
|
19
|
+
begin
|
|
20
|
+
require 'lint_roller' # RuboCop >= 1.72's plugin API
|
|
21
|
+
rescue LoadError
|
|
22
|
+
nil
|
|
23
|
+
else
|
|
24
|
+
require_relative 'rubocop/plugin'
|
|
25
|
+
end
|