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
data/sig/actionable.rbs
ADDED
|
@@ -0,0 +1,2593 @@
|
|
|
1
|
+
# Actionable: typed, composable Ruby service objects.
|
|
2
|
+
#
|
|
3
|
+
# This is the core entry point. Per decision D14, every core file gets an
|
|
4
|
+
# explicit +require_relative+ here — no autoload, no Zeitwerk. The optional
|
|
5
|
+
# adapters (+actionable/rails+, +actionable/rspec+) are separate requires and
|
|
6
|
+
# are deliberately NOT loaded here, keeping the core Rails-free.
|
|
7
|
+
module Actionable
|
|
8
|
+
HALT: Symbol
|
|
9
|
+
VERSION: String
|
|
10
|
+
|
|
11
|
+
# Turn the first-run verb-position check (owner rulings R5, R10) on or
|
|
12
|
+
# off for the whole process. Off, {Action.run} never parses a method's
|
|
13
|
+
# source and never loads Prism; the per-call code check in +fail+ / +skip+
|
|
14
|
+
# stays on. Meant as an escape hatch while upgrading (set it in an
|
|
15
|
+
# initializer), with the +Actionable/VerbNotLast+ cop as the only guard
|
|
16
|
+
# left. A class first run while off is checked on its first run after the
|
|
17
|
+
# check is turned back on.
|
|
18
|
+
#
|
|
19
|
+
# Actionable.verb_position_check = false
|
|
20
|
+
#
|
|
21
|
+
# _@param_ `enabled` — +false+ to skip the check
|
|
22
|
+
#
|
|
23
|
+
# _@return_ — whether the check is now on
|
|
24
|
+
def self.verb_position_check=: (bool enabled) -> bool
|
|
25
|
+
|
|
26
|
+
# Whether {Action.run} checks where each step and hook method places its
|
|
27
|
+
# plain recording verbs on an action's first run. On by default.
|
|
28
|
+
def self.verb_position_check?: () -> bool
|
|
29
|
+
|
|
30
|
+
# Run +block+ inside the halt catch point.
|
|
31
|
+
#
|
|
32
|
+
# Returns the block's value when nothing is thrown, or the value passed with
|
|
33
|
+
# +throw Actionable::HALT, value+ when the pipeline halts (+nil+ for a bare
|
|
34
|
+
# +throw Actionable::HALT+). A raised StandardError is deliberately NOT
|
|
35
|
+
# caught — it propagates to the caller (decision D4).
|
|
36
|
+
#
|
|
37
|
+
# _@return_ — the block's value, or the thrown halt value
|
|
38
|
+
def catch_halt: () ?{ () -> Object } -> Object
|
|
39
|
+
|
|
40
|
+
# Run +block+ inside the halt catch point.
|
|
41
|
+
#
|
|
42
|
+
# Returns the block's value when nothing is thrown, or the value passed with
|
|
43
|
+
# +throw Actionable::HALT, value+ when the pipeline halts (+nil+ for a bare
|
|
44
|
+
# +throw Actionable::HALT+). A raised StandardError is deliberately NOT
|
|
45
|
+
# caught — it propagates to the caller (decision D4).
|
|
46
|
+
#
|
|
47
|
+
# _@return_ — the block's value, or the thrown halt value
|
|
48
|
+
def self.catch_halt: () ?{ () -> Object } -> Object
|
|
49
|
+
|
|
50
|
+
# _@return_ — the process-wide action registry
|
|
51
|
+
def self.registry: () -> Registry
|
|
52
|
+
|
|
53
|
+
# Runs an action inside a background job and maps its result to a queue
|
|
54
|
+
# disposition: a success or skip is done (+:ack+); a failure either retries
|
|
55
|
+
# (transient) or is discarded (permanent). Genuine exceptions are never
|
|
56
|
+
# swallowed — they propagate so the queue's own retry/backoff handles them
|
|
57
|
+
# (invariant 2).
|
|
58
|
+
module Job
|
|
59
|
+
DEFAULT_PERMANENT_CODES: ::Array[Symbol]
|
|
60
|
+
|
|
61
|
+
# Map a result to a queue disposition.
|
|
62
|
+
#
|
|
63
|
+
# With +honor_retry: true+, a failure or skip whose metadata has a +retry:+
|
|
64
|
+
# key is decided by it instead: truthy is +:retry+, falsy is +:discard+ for
|
|
65
|
+
# a failure and +:ack+ for a skip. Results without the key, and successes,
|
|
66
|
+
# map as usual.
|
|
67
|
+
#
|
|
68
|
+
# _@param_ `result` — the action's result
|
|
69
|
+
#
|
|
70
|
+
# _@param_ `permanent` — failure codes treated as non-retryable
|
|
71
|
+
#
|
|
72
|
+
# _@param_ `honor_retry` — act on the result's +retry:+ metadata
|
|
73
|
+
#
|
|
74
|
+
# _@return_ — +:ack+ for a success or skip; +:discard+
|
|
75
|
+
# for a failure whose code is +permanent+; +:retry+ for any other failure
|
|
76
|
+
def disposition: (Result result, ?permanent: ::Array[Symbol], ?honor_retry: bool) -> Symbol
|
|
77
|
+
|
|
78
|
+
# Map a result to a queue disposition.
|
|
79
|
+
#
|
|
80
|
+
# With +honor_retry: true+, a failure or skip whose metadata has a +retry:+
|
|
81
|
+
# key is decided by it instead: truthy is +:retry+, falsy is +:discard+ for
|
|
82
|
+
# a failure and +:ack+ for a skip. Results without the key, and successes,
|
|
83
|
+
# map as usual.
|
|
84
|
+
#
|
|
85
|
+
# _@param_ `result` — the action's result
|
|
86
|
+
#
|
|
87
|
+
# _@param_ `permanent` — failure codes treated as non-retryable
|
|
88
|
+
#
|
|
89
|
+
# _@param_ `honor_retry` — act on the result's +retry:+ metadata
|
|
90
|
+
#
|
|
91
|
+
# _@return_ — +:ack+ for a success or skip; +:discard+
|
|
92
|
+
# for a failure whose code is +permanent+; +:retry+ for any other failure
|
|
93
|
+
def self.disposition: (Result result, ?permanent: ::Array[Symbol], ?honor_retry: bool) -> Symbol
|
|
94
|
+
|
|
95
|
+
# _@param_ `result`
|
|
96
|
+
#
|
|
97
|
+
# _@return_ — whether the result carries +retry:+ metadata
|
|
98
|
+
def retry_declared?: (Result result) -> bool
|
|
99
|
+
|
|
100
|
+
# _@param_ `result`
|
|
101
|
+
#
|
|
102
|
+
# _@return_ — whether the result carries +retry:+ metadata
|
|
103
|
+
def self.retry_declared?: (Result result) -> bool
|
|
104
|
+
|
|
105
|
+
# _@param_ `result` — a result whose metadata has +retry:+
|
|
106
|
+
def metadata_disposition: ((Failure | Skipped) result) -> Symbol
|
|
107
|
+
|
|
108
|
+
# _@param_ `result` — a result whose metadata has +retry:+
|
|
109
|
+
def self.metadata_disposition: ((Failure | Skipped) result) -> Symbol
|
|
110
|
+
|
|
111
|
+
# Raised by {Mixin#run_actionable} for a retryable failure, so the host
|
|
112
|
+
# queue (Sidekiq / ActiveJob) sees a raised error and applies its retry
|
|
113
|
+
# policy. Carries the offending result.
|
|
114
|
+
class RetryableFailure < StandardError
|
|
115
|
+
# _@param_ `result` — a {Failure}, or a {Skipped} retried because its metadata says +retry: true+ (see {RetryMetadata#honor_retry_metadata})
|
|
116
|
+
def initialize: (Result result) -> void
|
|
117
|
+
|
|
118
|
+
# _@return_ — the failed result that triggered the retry
|
|
119
|
+
attr_reader result: Result
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# The +honor_retry_metadata+ class macro (owner ruling P10). Extended onto
|
|
123
|
+
# {Actionable::Action} by +actionable/job+, so an action can opt in to having
|
|
124
|
+
# {Mixin#run_actionable} act on the +retry:+ metadata of its declared codes.
|
|
125
|
+
# Metadata stays neutral by default (decisions D20/D21).
|
|
126
|
+
module RetryMetadata
|
|
127
|
+
# Opt this action in (or, with +enabled: false+, back out) to {Job}
|
|
128
|
+
# honoring the +retry:+ metadata of its declared failures and skips:
|
|
129
|
+
# +retry: true+ retries the job, +retry: false+ does not. A code with no
|
|
130
|
+
# +retry:+ key keeps the default mapping. Inherited; a subclass can turn
|
|
131
|
+
# it off.
|
|
132
|
+
#
|
|
133
|
+
# class Reindex < Actionable::Action
|
|
134
|
+
# honor_retry_metadata
|
|
135
|
+
# skips :not_ready, retry: true # the job retries until the index is ready
|
|
136
|
+
# end
|
|
137
|
+
#
|
|
138
|
+
# _@param_ `enabled`
|
|
139
|
+
def honor_retry_metadata: (?enabled: bool) -> void
|
|
140
|
+
|
|
141
|
+
# Whether {Job} honors this action's +retry:+ metadata, inherited from
|
|
142
|
+
# ancestors when not set locally. +false+ unless an ancestor opted in.
|
|
143
|
+
def honor_retry_metadata?: () -> bool
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Mix into a Sidekiq worker or ActiveJob job. Call {#run_actionable} from
|
|
147
|
+
# +perform+; it runs the action, records the result on {#actionable_result},
|
|
148
|
+
# and raises {RetryableFailure} for a retryable failure (so the queue
|
|
149
|
+
# retries) — returning normally for +:ack+ and +:discard+.
|
|
150
|
+
#
|
|
151
|
+
# class ProjectEventJob
|
|
152
|
+
# include Sidekiq::Job
|
|
153
|
+
# include Actionable::Job::Mixin
|
|
154
|
+
# def perform(event_id) = run_actionable(ProjectApiEvent, event_id: event_id)
|
|
155
|
+
# end
|
|
156
|
+
module Mixin
|
|
157
|
+
# Run +action_class+ with the given arguments, map the outcome, and either
|
|
158
|
+
# return the result (+:ack+ / +:discard+) or raise {RetryableFailure}
|
|
159
|
+
# (+:retry+). A genuine exception inside the action propagates unchanged.
|
|
160
|
+
# When the action opted in with +honor_retry_metadata+, its declared
|
|
161
|
+
# +retry:+ metadata decides (see {Job.disposition}).
|
|
162
|
+
#
|
|
163
|
+
# _@param_ `action_class`
|
|
164
|
+
#
|
|
165
|
+
# _@param_ `args` — positional arguments, forwarded to +.run+
|
|
166
|
+
#
|
|
167
|
+
# _@param_ `kwargs` — keyword arguments, forwarded to +.run+
|
|
168
|
+
#
|
|
169
|
+
# _@return_ — the run's result (for an ack/discard outcome)
|
|
170
|
+
def run_actionable: (singleton(Actionable::Action) action_class, *Object args, **Object kwargs) -> Result
|
|
171
|
+
|
|
172
|
+
# _@return_ — failure codes this job treats as non-retryable;
|
|
173
|
+
# override to customize. Defaults to {DEFAULT_PERMANENT_CODES}.
|
|
174
|
+
def actionable_permanent_codes: () -> ::Array[Symbol]
|
|
175
|
+
|
|
176
|
+
# _@return_ — the result of the most recent {#run_actionable}
|
|
177
|
+
attr_reader actionable_result: Result?
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Generates RBS for a *user* action class — track 2 of the RBS strategy
|
|
182
|
+
# (decision D13). Sord covers the library's static classes, but it can't see
|
|
183
|
+
# +CreateInvoice.run(amount:)+ or +result.invoice+: those depend on the
|
|
184
|
+
# action's declared +input+/+output+ schemas. This walks those schemas (via
|
|
185
|
+
# FieldStruct's metadata and +FieldStruct::RBS.type_for+) and emits:
|
|
186
|
+
#
|
|
187
|
+
# * a typed +.run+ signature from the input schema (or +(*untyped)+ when
|
|
188
|
+
# free-form), with an overload taking a built input struct;
|
|
189
|
+
# * for a fixed +input+ schema, a nested +InputSchema+ struct and a typed
|
|
190
|
+
# +#input+ reader returning it. Like +Output+ and +Result+ it exists only
|
|
191
|
+
# in RBS, so generation raises +ArgumentError+ when Ruby already resolves
|
|
192
|
+
# +Action::InputSchema+;
|
|
193
|
+
# * a nullable instance variable per output field (+@total: ::Integer?+),
|
|
194
|
+
# so steps that assign it type-check. The generated RBS owns these: a
|
|
195
|
+
# host sig must not declare them again, since RBS rejects an instance
|
|
196
|
+
# variable declared twice;
|
|
197
|
+
# * a nested +Output+ struct with a typed accessor per output field;
|
|
198
|
+
# * a nested +Result+ with +output+ and the convenience-delegation readers,
|
|
199
|
+
# which +.run+ returns;
|
|
200
|
+
# * when the action declares failures with +fails+, a +failure_code+ type
|
|
201
|
+
# alias listing every code it can fail with: the declared ones, then the
|
|
202
|
+
# {Action::GEM_FAILURE_CODES} (decision D20);
|
|
203
|
+
# * when the action declares skips with +skips+, a +skip_code+ type alias
|
|
204
|
+
# listing every code it can skip with (decision D21). The gem emits no skip
|
|
205
|
+
# codes of its own, so the alias holds the declared ones only.
|
|
206
|
+
#
|
|
207
|
+
# Under typed outcomes (decision D23) +.run+ returns the action's own
|
|
208
|
+
# +Success | Skipped | Failure+ instead, each typed with its +code+ and an
|
|
209
|
+
# +output+ that names what the outcome can carry: a nested class per code
|
|
210
|
+
# that declares an output schema (+AlreadyActiveSkipOutput+,
|
|
211
|
+
# +InvalidFailureOutput+), and a Hash for a code without one. A failure's
|
|
212
|
+
# output also admits every skip schema and +Output+, since a broken one ends
|
|
213
|
+
# the run as +invalid_output+ carrying it.
|
|
214
|
+
#
|
|
215
|
+
# class CreateInvoice < Actionable::Action
|
|
216
|
+
# input { required :amount, :integer }
|
|
217
|
+
# output { required :invoice, :string }
|
|
218
|
+
# end
|
|
219
|
+
#
|
|
220
|
+
# puts Actionable::RBS.generate(CreateInvoice)
|
|
221
|
+
# # class CreateInvoice < ::Actionable::Action
|
|
222
|
+
# # def self.run: (amount: ::Integer) -> Result
|
|
223
|
+
# # | (InputSchema input) -> Result
|
|
224
|
+
# # def input: () -> InputSchema
|
|
225
|
+
# # @invoice: ::String?
|
|
226
|
+
# # class InputSchema < ::FieldStruct::Base ... end
|
|
227
|
+
# # class Output < ::FieldStruct::Base ... end
|
|
228
|
+
# # class Result < ::Actionable::Result ... end
|
|
229
|
+
# # end
|
|
230
|
+
#
|
|
231
|
+
# The action names its real superclass, and every named ancestor up to
|
|
232
|
+
# {Actionable::Action} is declared before it, member-less, so the output
|
|
233
|
+
# resolves without the host generating its abstract bases.
|
|
234
|
+
#
|
|
235
|
+
# The emitted +Result+/+Output+/+InputSchema+ are generated type aids: at
|
|
236
|
+
# runtime +.run+ returns a {Success}/{Failure}/{Skipped} that responds to
|
|
237
|
+
# these via output delegation, and the schemas are anonymous classes. Field
|
|
238
|
+
# types come from +FieldStruct::RBS.type_for+, so they match FieldStruct's own
|
|
239
|
+
# generator, and describe a valid instance. Referenced field types appear as
|
|
240
|
+
# qualified names (+::Invoice+) — generate RBS for those (e.g. via
|
|
241
|
+
# +FieldStruct::RBS+) so they resolve.
|
|
242
|
+
module RBS
|
|
243
|
+
|
|
244
|
+
# Generate RBS source for a user action class.
|
|
245
|
+
#
|
|
246
|
+
# _@param_ `klass`
|
|
247
|
+
#
|
|
248
|
+
# _@return_ — RBS for the action, wrapped in its module nesting
|
|
249
|
+
def self.generate: (singleton(Actionable::Action) klass) -> String
|
|
250
|
+
|
|
251
|
+
# The generated input type is named +InputSchema+, nested in the action.
|
|
252
|
+
# It exists only in RBS, so a Ruby constant that +klass::InputSchema+
|
|
253
|
+
# already resolves to (on the action, an ancestor class or an included
|
|
254
|
+
# module; +Object+'s top-level constants don't resolve through +klass::+)
|
|
255
|
+
# would make the RBS name lie.
|
|
256
|
+
#
|
|
257
|
+
# _@param_ `klass`
|
|
258
|
+
def self.check_input_schema_name!: (singleton(Actionable::Action) klass) -> void
|
|
259
|
+
|
|
260
|
+
# _@param_ `klass`
|
|
261
|
+
#
|
|
262
|
+
# _@return_ — whether the generated RBS declares +InputSchema+: for a
|
|
263
|
+
# fixed +input+ schema, not a discriminated +input_for+ one
|
|
264
|
+
def self.input_schema_class?: (singleton(Actionable::Action) klass) -> bool
|
|
265
|
+
|
|
266
|
+
# Declares every named ancestor between +klass+ and {Actionable::Action},
|
|
267
|
+
# so the action's superclass resolves on its own: a host often never
|
|
268
|
+
# generates its abstract base actions. Each is member-less, except that a
|
|
269
|
+
# typed ancestor also declares its result classes empty, for a typed
|
|
270
|
+
# subclass's result classes to subclass. Being empty, each declaration
|
|
271
|
+
# merges with the ancestor's full signature when that is generated too, and
|
|
272
|
+
# repeats byte-identically across the subclasses of one base, which RBS
|
|
273
|
+
# accepts across files.
|
|
274
|
+
#
|
|
275
|
+
# _@param_ `klass`
|
|
276
|
+
#
|
|
277
|
+
# _@return_ — one namespaced block per ancestor, outermost first
|
|
278
|
+
def self.ancestor_blocks: (singleton(Actionable::Action) klass) -> String
|
|
279
|
+
|
|
280
|
+
# _@param_ `ancestor`
|
|
281
|
+
#
|
|
282
|
+
# _@return_ — the ancestor's member-less class block
|
|
283
|
+
def self.ancestor_block: (singleton(Actionable::Action) ancestor) -> String
|
|
284
|
+
|
|
285
|
+
# _@param_ `klass`
|
|
286
|
+
#
|
|
287
|
+
# _@return_ — the named classes strictly
|
|
288
|
+
# between +klass+ and {Actionable::Action}, nearest first
|
|
289
|
+
def self.named_ancestors: (singleton(Actionable::Action) klass) -> ::Array[singleton(Actionable::Action)]
|
|
290
|
+
|
|
291
|
+
# _@param_ `klass`
|
|
292
|
+
#
|
|
293
|
+
# _@return_ — +class Name < ::Superclass+, naming the nearest named
|
|
294
|
+
# superclass (anonymous ones have no RBS name)
|
|
295
|
+
def self.class_head: (singleton(Actionable::Action) klass) -> String
|
|
296
|
+
|
|
297
|
+
# _@param_ `parent`
|
|
298
|
+
#
|
|
299
|
+
# _@return_ — whether a subclass's result classes subclass +parent+'s
|
|
300
|
+
def self.typed_parent?: (Class parent) -> bool
|
|
301
|
+
|
|
302
|
+
# _@param_ `klass`
|
|
303
|
+
def self.class_block: (singleton(Actionable::Action) klass) -> String
|
|
304
|
+
|
|
305
|
+
# The class block of an action with typed outcomes (decision D23).
|
|
306
|
+
#
|
|
307
|
+
# _@param_ `klass`
|
|
308
|
+
def self.typed_class_block: (singleton(Actionable::Action) klass) -> String
|
|
309
|
+
|
|
310
|
+
# The sections every action's class block opens with: the code aliases,
|
|
311
|
+
# +.run+, the typed +#input+ reader, the output instance variables, and the
|
|
312
|
+
# +InputSchema+ class.
|
|
313
|
+
#
|
|
314
|
+
# _@param_ `klass`
|
|
315
|
+
#
|
|
316
|
+
# _@param_ `return_type` — what +.run+ returns
|
|
317
|
+
def self.leading_sections: (singleton(Actionable::Action) klass, String return_type) -> ::Array[String]
|
|
318
|
+
|
|
319
|
+
# One instance variable per output field, which steps assign and the
|
|
320
|
+
# {Runner} captures into the output. Each is nullable: it is unset until a
|
|
321
|
+
# step assigns it. The generator owns these declarations, since RBS rejects
|
|
322
|
+
# the same instance variable declared in two files.
|
|
323
|
+
#
|
|
324
|
+
# _@param_ `schema` — the success output schema
|
|
325
|
+
def self.output_ivar_lines: (singleton(FieldStruct::Base) schema) -> ::Array[String]
|
|
326
|
+
|
|
327
|
+
# The nested output class of every skip, then every failure, code that
|
|
328
|
+
# declares an output schema, named after the code.
|
|
329
|
+
#
|
|
330
|
+
# _@param_ `klass`
|
|
331
|
+
#
|
|
332
|
+
# _@return_ — name, schema
|
|
333
|
+
def self.outcome_schemas: (singleton(Actionable::Action) klass) -> ::Array[[String, singleton(FieldStruct::Base)]]
|
|
334
|
+
|
|
335
|
+
# Every output class name must be an RBS class name, and unique: two codes
|
|
336
|
+
# that camelize alike (+:ready+ / +:ready_+, +:expired?+ /
|
|
337
|
+
# +:expired_predicate+) would otherwise emit one class for two schemas.
|
|
338
|
+
#
|
|
339
|
+
# _@param_ `klass`
|
|
340
|
+
#
|
|
341
|
+
# _@param_ `kind` — +skip+ or +failure+
|
|
342
|
+
#
|
|
343
|
+
# _@param_ `entries` — code, class name, schema
|
|
344
|
+
def self.check_outcome_names!: (singleton(Actionable::Action) klass, String kind, ::Array[[Symbol, String, singleton(FieldStruct::Base)]] entries) -> void
|
|
345
|
+
|
|
346
|
+
# The +Success+, +Skipped+ and +Failure+ blocks of a typed action.
|
|
347
|
+
#
|
|
348
|
+
# _@param_ `klass`
|
|
349
|
+
def self.outcome_blocks: (singleton(Actionable::Action) klass) -> ::Array[String]
|
|
350
|
+
|
|
351
|
+
# The readers a result delegates to its output struct, one per field any
|
|
352
|
+
# of +schemas+ declares. A field several declare types as the union of
|
|
353
|
+
# their types, nullable when any declares it optional, or when +nullable+.
|
|
354
|
+
# A failure's readers are all nullable: an +invalid_output+ failure carries
|
|
355
|
+
# the output that broke its schema, so a required field can be missing.
|
|
356
|
+
#
|
|
357
|
+
# _@param_ `schemas`
|
|
358
|
+
#
|
|
359
|
+
# _@param_ `nullable` — make every reader nullable
|
|
360
|
+
def self.delegator_lines: (::Array[singleton(FieldStruct::Base)] schemas, ?nullable: bool) -> ::Array[String]
|
|
361
|
+
|
|
362
|
+
# _@param_ `klass`
|
|
363
|
+
#
|
|
364
|
+
# _@return_ — the +Success+ block's signatures
|
|
365
|
+
def self.success_lines: (singleton(Actionable::Action) klass) -> ::Array[String]
|
|
366
|
+
|
|
367
|
+
# _@param_ `declared` — the declared codes
|
|
368
|
+
#
|
|
369
|
+
# _@param_ `name` — the alias naming them
|
|
370
|
+
#
|
|
371
|
+
# _@return_ — the alias, or +::Symbol+ when none are declared
|
|
372
|
+
def self.code_type: (::Hash[untyped, untyped] declared, String name) -> String
|
|
373
|
+
|
|
374
|
+
# _@param_ `types` — the output classes an outcome can carry
|
|
375
|
+
#
|
|
376
|
+
# _@return_ — them and a Hash, as one RBS type
|
|
377
|
+
def self.union: (::Array[String] types) -> String
|
|
378
|
+
|
|
379
|
+
# _@param_ `klass`
|
|
380
|
+
#
|
|
381
|
+
# _@param_ `name` — +Success+, +Skipped+ or +Failure+
|
|
382
|
+
#
|
|
383
|
+
# _@param_ `lines` — the block's method signatures
|
|
384
|
+
#
|
|
385
|
+
# _@return_ — the class block, subclassing the parent action's own
|
|
386
|
+
# result class when the parent has typed outcomes, else the gem's
|
|
387
|
+
def self.outcome_block: (singleton(Actionable::Action) klass, String name, ::Array[String] lines) -> String
|
|
388
|
+
|
|
389
|
+
# _@param_ `parent` — the superclass of the action being generated
|
|
390
|
+
#
|
|
391
|
+
# _@param_ `name` — +Success+, +Skipped+ or +Failure+
|
|
392
|
+
#
|
|
393
|
+
# _@return_ — the parent's result class when it has typed outcomes,
|
|
394
|
+
# else the gem's
|
|
395
|
+
def self.outcome_base: (Class parent, String name) -> String
|
|
396
|
+
|
|
397
|
+
# A code's trailing +?+ / +!+ becomes +Predicate+ / +Bang+, since neither
|
|
398
|
+
# may appear in a constant; +:x+ and +:x?+ get different names.
|
|
399
|
+
#
|
|
400
|
+
# _@param_ `code`
|
|
401
|
+
#
|
|
402
|
+
# _@return_ — +:already_active+ as +AlreadyActive+, +:expired?+ as
|
|
403
|
+
# +ExpiredPredicate+, +:done!+ as +DoneBang+
|
|
404
|
+
def self.camelize: (Symbol code) -> String
|
|
405
|
+
|
|
406
|
+
# _@param_ `klass`
|
|
407
|
+
#
|
|
408
|
+
# _@param_ `return_type` — what +.run+ returns
|
|
409
|
+
#
|
|
410
|
+
# _@return_ — the +.run+ signature: typed kwargs from the input schema
|
|
411
|
+
# (or +(*untyped)+ free-form), plus an overload taking a built
|
|
412
|
+
# +InputSchema+ instance, which +.run+ passes through
|
|
413
|
+
def self.run_signature: (singleton(Actionable::Action) klass, return_type: String) -> String
|
|
414
|
+
|
|
415
|
+
# _@param_ `klass` — an action that declares failures
|
|
416
|
+
#
|
|
417
|
+
# _@return_ — the +failure_code+ alias: a union of the declared codes
|
|
418
|
+
# and the codes the gem emits itself, as Symbol literal types
|
|
419
|
+
def self.failure_code_alias: (singleton(Actionable::Action) klass) -> String
|
|
420
|
+
|
|
421
|
+
# _@param_ `klass` — an action that declares skips
|
|
422
|
+
#
|
|
423
|
+
# _@return_ — the +skip_code+ alias: a union of the declared skip codes
|
|
424
|
+
# as Symbol literal types
|
|
425
|
+
def self.skip_code_alias: (singleton(Actionable::Action) klass) -> String
|
|
426
|
+
|
|
427
|
+
# _@param_ `field`
|
|
428
|
+
#
|
|
429
|
+
# _@return_ — +name: Type+ (required), +?name: Type+ (required with a
|
|
430
|
+
# default, so it may be omitted) or +?name: Type?+ (optional: it may be
|
|
431
|
+
# omitted or passed +nil+; +run(note: nil)+ succeeds).
|
|
432
|
+
def self.run_param: (FieldStruct::Field field) -> String
|
|
433
|
+
|
|
434
|
+
# A nested struct class with FieldStruct's reader and writer shape: the
|
|
435
|
+
# reader nullable when optional, the writer +(T? value) -> T?+.
|
|
436
|
+
#
|
|
437
|
+
# _@param_ `schema` — an output or input schema
|
|
438
|
+
#
|
|
439
|
+
# _@param_ `name` — the nested class name
|
|
440
|
+
#
|
|
441
|
+
# _@return_ — the nested class block
|
|
442
|
+
def self.output_block: (singleton(FieldStruct::Base) schema, ?String name) -> String
|
|
443
|
+
|
|
444
|
+
# _@param_ `schema` — the output schema
|
|
445
|
+
#
|
|
446
|
+
# _@return_ — the nested +Result+ class block with output delegation
|
|
447
|
+
def self.result_block: (singleton(FieldStruct::Base) schema) -> String
|
|
448
|
+
|
|
449
|
+
# The field's RBS type, as FieldStruct's own generator emits it: made
|
|
450
|
+
# nullable when optional, or as +nullable+ says (D13).
|
|
451
|
+
#
|
|
452
|
+
# _@param_ `field`
|
|
453
|
+
#
|
|
454
|
+
# _@param_ `nullable` — +nil+ follows the field's optionality
|
|
455
|
+
def self.rbs_type: (FieldStruct::Field field, ?nullable: bool?) -> String
|
|
456
|
+
|
|
457
|
+
# _@param_ `type` — an RBS type expression
|
|
458
|
+
#
|
|
459
|
+
# _@return_ — +type+ made nullable, unless it already admits +nil+
|
|
460
|
+
def self.nullable: (String _type) -> String
|
|
461
|
+
|
|
462
|
+
# Wrap a class block in the nesting implied by its qualified name,
|
|
463
|
+
# indenting two spaces per level. A namespace that is a class opens as
|
|
464
|
+
# +class+ (no superclass, so it merges with the class's own declaration),
|
|
465
|
+
# since RBS rejects a class and a module of the same name.
|
|
466
|
+
#
|
|
467
|
+
# _@param_ `full_name`
|
|
468
|
+
#
|
|
469
|
+
# _@param_ `inner`
|
|
470
|
+
#
|
|
471
|
+
# _@return_ — RBS source ending in a trailing newline
|
|
472
|
+
def self.wrap_namespace: (String full_name, String inner) -> String
|
|
473
|
+
|
|
474
|
+
# _@param_ `text`
|
|
475
|
+
#
|
|
476
|
+
# _@return_ — +text+ with every non-blank line indented two spaces
|
|
477
|
+
def self.indent: (String text) -> String
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
# RSpec matchers and stubs for actions (decision D12).
|
|
481
|
+
module RSpec
|
|
482
|
+
# The refusal for a stub, +and_fail+ or +and_skip+ naming a code an action
|
|
483
|
+
# that declares failures (decision D20) or skips (decision D21) doesn't
|
|
484
|
+
# declare.
|
|
485
|
+
#
|
|
486
|
+
# _@param_ `action_class`
|
|
487
|
+
#
|
|
488
|
+
# _@param_ `code`
|
|
489
|
+
#
|
|
490
|
+
# _@param_ `outcome` — which declarations the code was checked against
|
|
491
|
+
def self.undeclared_code_message: (singleton(Actionable::Action) action_class, (Symbol | String) code, ?outcome: Symbol) -> String
|
|
492
|
+
|
|
493
|
+
# Helpers mixed into example groups by +actionable/rspec+ that stub an
|
|
494
|
+
# action's +.run+ to return a real {Success} / {Failure} built from a hash,
|
|
495
|
+
# so callers can assert on +result.code+/+output+/+errors+ without executing
|
|
496
|
+
# the action (decision D12).
|
|
497
|
+
#
|
|
498
|
+
# +allow_*+ permits the call (a stub); +stub_*+ also sets a call expectation
|
|
499
|
+
# (a mock, verified on the example) — mirroring RSpec's allow/expect split.
|
|
500
|
+
#
|
|
501
|
+
# A stubbed failure of an action that declares failures with
|
|
502
|
+
# {Actionable::Action.fails} carries the declared metadata (and message,
|
|
503
|
+
# when none is given) for its code, and a code the action doesn't declare
|
|
504
|
+
# raises +ArgumentError+, so a stub can't return what the action never
|
|
505
|
+
# could (decision D20). A stubbed skip follows {Actionable::Action.skips}
|
|
506
|
+
# the same way (decision D21), the default +:skipped+ included.
|
|
507
|
+
module Stubs
|
|
508
|
+
# _@param_ `action_class` — the action whose +.run+ is stubbed
|
|
509
|
+
#
|
|
510
|
+
# _@param_ `opts` — {#actionable_success_result}'s keywords (+message:+, +output:+, +errors:+)
|
|
511
|
+
#
|
|
512
|
+
# _@return_ — the stubbed result
|
|
513
|
+
def allow_actionable_success: (singleton(Actionable::Action) action_class, **Object opts) -> Success
|
|
514
|
+
|
|
515
|
+
# _@param_ `action_class` — the action whose +.run+ is stubbed
|
|
516
|
+
#
|
|
517
|
+
# _@param_ `opts` — {#actionable_success_result}'s keywords (+message:+, +output:+, +errors:+)
|
|
518
|
+
#
|
|
519
|
+
# _@return_ — the stubbed result
|
|
520
|
+
def stub_actionable_success: (singleton(Actionable::Action) action_class, **Object opts) -> Success
|
|
521
|
+
|
|
522
|
+
# _@param_ `action_class` — the action whose +.run+ is stubbed
|
|
523
|
+
#
|
|
524
|
+
# _@param_ `opts` — {#actionable_failure_result}'s keywords; +code:+ defaults to +:error+
|
|
525
|
+
#
|
|
526
|
+
# _@return_ — the stubbed result
|
|
527
|
+
def allow_actionable_failure: (singleton(Actionable::Action) action_class, **Object opts) -> Failure
|
|
528
|
+
|
|
529
|
+
# _@param_ `action_class` — the action whose +.run+ is stubbed
|
|
530
|
+
#
|
|
531
|
+
# _@param_ `opts` — {#actionable_failure_result}'s keywords; +code:+ defaults to +:error+
|
|
532
|
+
#
|
|
533
|
+
# _@return_ — the stubbed result
|
|
534
|
+
def stub_actionable_failure: (singleton(Actionable::Action) action_class, **Object opts) -> Failure
|
|
535
|
+
|
|
536
|
+
# _@param_ `action_class` — the action whose +.run+ is stubbed
|
|
537
|
+
#
|
|
538
|
+
# _@param_ `opts` — {#actionable_skip_result}'s keywords; +code:+ defaults to +:skipped+
|
|
539
|
+
#
|
|
540
|
+
# _@return_ — the stubbed result
|
|
541
|
+
def allow_actionable_skip: (singleton(Actionable::Action) action_class, **Object opts) -> Skipped
|
|
542
|
+
|
|
543
|
+
# _@param_ `action_class` — the action whose +.run+ is stubbed
|
|
544
|
+
#
|
|
545
|
+
# _@param_ `opts` — {#actionable_skip_result}'s keywords; +code:+ defaults to +:skipped+
|
|
546
|
+
#
|
|
547
|
+
# _@return_ — the stubbed result
|
|
548
|
+
def stub_actionable_skip: (singleton(Actionable::Action) action_class, **Object opts) -> Skipped
|
|
549
|
+
|
|
550
|
+
# Build a real {Success} from a hash (no stubbing).
|
|
551
|
+
#
|
|
552
|
+
# _@param_ `message`
|
|
553
|
+
#
|
|
554
|
+
# _@param_ `output`
|
|
555
|
+
#
|
|
556
|
+
# _@param_ `errors`
|
|
557
|
+
def actionable_success_result: (?message: String?, ?output: (::Hash[Symbol, Object] | FieldStruct::Base), ?errors: ::Hash[Symbol, (String | ::Array[String])]) -> Success
|
|
558
|
+
|
|
559
|
+
# Build a real {Failure} from a hash (no stubbing).
|
|
560
|
+
#
|
|
561
|
+
# _@param_ `code`
|
|
562
|
+
#
|
|
563
|
+
# _@param_ `message`
|
|
564
|
+
#
|
|
565
|
+
# _@param_ `output`
|
|
566
|
+
#
|
|
567
|
+
# _@param_ `errors`
|
|
568
|
+
#
|
|
569
|
+
# _@param_ `metadata` — the failure's per-code metadata
|
|
570
|
+
def actionable_failure_result: (
|
|
571
|
+
?code: Symbol,
|
|
572
|
+
?message: String?,
|
|
573
|
+
?output: (::Hash[Symbol, Object] | FieldStruct::Base),
|
|
574
|
+
?errors: ::Hash[Symbol, (String | ::Array[String])],
|
|
575
|
+
?metadata: ::Hash[Symbol, Object]
|
|
576
|
+
) -> Failure
|
|
577
|
+
|
|
578
|
+
# Build a real {Skipped} from a hash (no stubbing).
|
|
579
|
+
#
|
|
580
|
+
# _@param_ `code`
|
|
581
|
+
#
|
|
582
|
+
# _@param_ `message`
|
|
583
|
+
#
|
|
584
|
+
# _@param_ `output`
|
|
585
|
+
#
|
|
586
|
+
# _@param_ `metadata` — the skip's per-code metadata
|
|
587
|
+
def actionable_skip_result: (
|
|
588
|
+
?code: Symbol,
|
|
589
|
+
?message: String?,
|
|
590
|
+
?output: (::Hash[Symbol, Object] | FieldStruct::Base),
|
|
591
|
+
?metadata: ::Hash[Symbol, Object]
|
|
592
|
+
) -> Skipped
|
|
593
|
+
|
|
594
|
+
# A {Failure} for +action_class+: the declared metadata and message for
|
|
595
|
+
# its code, or a refusal when the action declares failures but not it.
|
|
596
|
+
#
|
|
597
|
+
# _@param_ `action_class`
|
|
598
|
+
#
|
|
599
|
+
# _@param_ `code`
|
|
600
|
+
#
|
|
601
|
+
# _@param_ `opts` — the rest of {#actionable_failure_result}'s keywords
|
|
602
|
+
def declared_failure_result: (singleton(Actionable::Action) action_class, ?code: (Symbol | String), **Object opts) -> Failure
|
|
603
|
+
|
|
604
|
+
# A {Skipped} for +action_class+: the declared metadata and message for
|
|
605
|
+
# its code, or a refusal when the action declares skips but not it.
|
|
606
|
+
#
|
|
607
|
+
# _@param_ `action_class`
|
|
608
|
+
#
|
|
609
|
+
# _@param_ `code`
|
|
610
|
+
#
|
|
611
|
+
# _@param_ `opts` — the rest of {#actionable_skip_result}'s keywords
|
|
612
|
+
def declared_skip_result: (singleton(Actionable::Action) action_class, ?code: (Symbol | String), **Object opts) -> Skipped
|
|
613
|
+
|
|
614
|
+
# Permit +action_class.run+, answering +result+ as a real run would.
|
|
615
|
+
#
|
|
616
|
+
# _@param_ `action_class`
|
|
617
|
+
#
|
|
618
|
+
# _@param_ `result`
|
|
619
|
+
#
|
|
620
|
+
# _@return_ — the result the stub answers
|
|
621
|
+
def allow_run: (singleton(Actionable::Action) action_class, Result result) -> Result
|
|
622
|
+
|
|
623
|
+
# Expect +action_class.run+ (verified on the example), answering +result+
|
|
624
|
+
# as a real run would.
|
|
625
|
+
#
|
|
626
|
+
# _@param_ `action_class`
|
|
627
|
+
#
|
|
628
|
+
# _@param_ `result`
|
|
629
|
+
#
|
|
630
|
+
# _@return_ — the result the stub answers
|
|
631
|
+
def expect_run: (singleton(Actionable::Action) action_class, Result result) -> Result
|
|
632
|
+
|
|
633
|
+
# The stubbed result as a real run would return it (decision D23): its
|
|
634
|
+
# output shaped by the schema it is held to, then as the action's own
|
|
635
|
+
# result class when it has typed outcomes.
|
|
636
|
+
#
|
|
637
|
+
# _@param_ `action_class`
|
|
638
|
+
#
|
|
639
|
+
# _@param_ `result`
|
|
640
|
+
def as_action_result: (singleton(Actionable::Action) action_class, Result result) -> Result
|
|
641
|
+
|
|
642
|
+
# Mirror the runner: output for a code with a schema becomes that schema
|
|
643
|
+
# (refused when it breaks it, since a real run would end as
|
|
644
|
+
# +invalid_output+ instead), and under typed outcomes a code without one
|
|
645
|
+
# carries an empty output (refused when output is given, as a real run
|
|
646
|
+
# refuses it). The gem's own failure codes keep what they are given.
|
|
647
|
+
#
|
|
648
|
+
# _@param_ `action_class`
|
|
649
|
+
#
|
|
650
|
+
# _@param_ `result` — updated in place
|
|
651
|
+
def shape_output: (singleton(Actionable::Action) action_class, Result result) -> void
|
|
652
|
+
|
|
653
|
+
# _@param_ `action_class`
|
|
654
|
+
#
|
|
655
|
+
# _@param_ `result`
|
|
656
|
+
#
|
|
657
|
+
# _@return_ — the schema the result's output
|
|
658
|
+
# is held to: its code's, or the success schema under typed outcomes
|
|
659
|
+
def stubbed_output_schema: (singleton(Actionable::Action) action_class, Result result) -> singleton(FieldStruct::Base)?
|
|
660
|
+
|
|
661
|
+
# The stub's output as an instance of +schema+, built from the fields of
|
|
662
|
+
# +result+'s output that the schema declares.
|
|
663
|
+
#
|
|
664
|
+
# _@param_ `action_class`
|
|
665
|
+
#
|
|
666
|
+
# _@param_ `result`
|
|
667
|
+
#
|
|
668
|
+
# _@param_ `schema`
|
|
669
|
+
def build_stubbed_output: (singleton(Actionable::Action) action_class, Result result, singleton(FieldStruct::Base) schema) -> FieldStruct::Base
|
|
670
|
+
|
|
671
|
+
# The stub's response: +result+, noted as a stubbed run when outcome
|
|
672
|
+
# coverage is loaded (decision D22), so a report can tell an outcome
|
|
673
|
+
# only ever stubbed from one never reached at all.
|
|
674
|
+
#
|
|
675
|
+
# _@param_ `action_class`
|
|
676
|
+
#
|
|
677
|
+
# _@param_ `result`
|
|
678
|
+
#
|
|
679
|
+
# _@return_ — +result+
|
|
680
|
+
def stubbed_run: (singleton(Actionable::Action) action_class, Result result) -> Result
|
|
681
|
+
|
|
682
|
+
# Add +errors+ to +result+'s errors.
|
|
683
|
+
#
|
|
684
|
+
# _@param_ `result`
|
|
685
|
+
#
|
|
686
|
+
# _@param_ `errors`
|
|
687
|
+
#
|
|
688
|
+
# _@return_ — +result+
|
|
689
|
+
def apply_errors: (Result result, ::Hash[Symbol, (String | ::Array[String])] errors) -> Result
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
# RSpec matcher methods mixed into example groups by +actionable/rspec+.
|
|
693
|
+
module Matchers
|
|
694
|
+
# Run an action and assert on its outcome.
|
|
695
|
+
#
|
|
696
|
+
# expect(CreateInvoice).to perform_actionable(amount: 5).and_succeed
|
|
697
|
+
# expect(CreateInvoice).to perform_actionable(bad).and_fail(:invalid_input)
|
|
698
|
+
# expect(Boom).to perform_actionable.and_raise(RuntimeError)
|
|
699
|
+
#
|
|
700
|
+
# _@param_ `args` — positional args forwarded to +.run+
|
|
701
|
+
#
|
|
702
|
+
# _@param_ `kwargs` — keyword args forwarded to +.run+
|
|
703
|
+
def perform_actionable: (*Object args, **Object kwargs) ?{ (Result? result, StandardError? exception) -> Object } -> PerformActionable
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
# The matcher object returned by {Matchers#perform_actionable}. Runs the
|
|
707
|
+
# action under test and checks the configured outcome (decision D12). Bare
|
|
708
|
+
# +perform_actionable+ expects a success; chain +and_succeed+ / +and_fail+ /
|
|
709
|
+
# +and_raise+ to be explicit. Code and message expectations match with +===+,
|
|
710
|
+
# so exact values, Regexps, and classes all work.
|
|
711
|
+
class PerformActionable
|
|
712
|
+
DEFAULT_BACKTRACE_QTY: Integer
|
|
713
|
+
|
|
714
|
+
# _@param_ `args` — positional args forwarded to +.run+
|
|
715
|
+
#
|
|
716
|
+
# _@param_ `kwargs` — keyword args forwarded to +.run+
|
|
717
|
+
#
|
|
718
|
+
# _@param_ `block` — extra assertions, called with the result and exception
|
|
719
|
+
def initialize: (::Array[Object] args, ::Hash[Symbol, Object] kwargs, Proc? block) -> void
|
|
720
|
+
|
|
721
|
+
# Expect a successful result, optionally matching +message+.
|
|
722
|
+
#
|
|
723
|
+
# _@param_ `message` — matched against the result's message with +===+
|
|
724
|
+
def and_succeed: (?Object? message) ?{ (Result? result, StandardError? exception) -> Object } -> self
|
|
725
|
+
|
|
726
|
+
# Expect a failed result, optionally matching +code+ and +message+. On an
|
|
727
|
+
# action that declares failures, a Symbol +code+ it doesn't declare raises
|
|
728
|
+
# +ArgumentError+ (decision D20).
|
|
729
|
+
#
|
|
730
|
+
# _@param_ `code` — matched against the result's code with +==+
|
|
731
|
+
#
|
|
732
|
+
# _@param_ `message` — matched against the result's message with +===+
|
|
733
|
+
def and_fail: (?Object? code, ?Object? message) ?{ (Result? result, StandardError? exception) -> Object } -> self
|
|
734
|
+
|
|
735
|
+
# Expect a skipped result, optionally matching the reason +code+ and
|
|
736
|
+
# +message+ (decision D17). On an action that declares skips, a Symbol
|
|
737
|
+
# +code+ it doesn't declare raises +ArgumentError+ (decision D21).
|
|
738
|
+
#
|
|
739
|
+
# _@param_ `code` — matched against the result's code with +==+
|
|
740
|
+
#
|
|
741
|
+
# _@param_ `message` — matched against the result's message with +===+
|
|
742
|
+
def and_skip: (?Object? code, ?Object? message) ?{ (Result? result, StandardError? exception) -> Object } -> self
|
|
743
|
+
|
|
744
|
+
# Expect the run to raise +error_class+, optionally matching +message+.
|
|
745
|
+
#
|
|
746
|
+
# _@param_ `error_class`
|
|
747
|
+
#
|
|
748
|
+
# _@param_ `message` — matched against the exception's message with +===+
|
|
749
|
+
def and_raise: (?singleton(Exception) error_class, ?Object? message) ?{ (Result? result, StandardError? exception) -> Object } -> self
|
|
750
|
+
|
|
751
|
+
# _@param_ `action_class`
|
|
752
|
+
def matches?: (singleton(Actionable::Action) action_class) -> bool
|
|
753
|
+
|
|
754
|
+
# _@param_ `action_class`
|
|
755
|
+
def does_not_match?: (singleton(Actionable::Action) action_class) -> bool
|
|
756
|
+
|
|
757
|
+
def failure_message: () -> String
|
|
758
|
+
|
|
759
|
+
def failure_message_when_negated: () -> String
|
|
760
|
+
|
|
761
|
+
def description: () -> String
|
|
762
|
+
|
|
763
|
+
# _@param_ `outcome` — +:success+, +:failure+, +:skip+ or +:raise+
|
|
764
|
+
#
|
|
765
|
+
# _@param_ `message`
|
|
766
|
+
def configure: (Symbol outcome, Object? message) ?{ (Result? result, StandardError? exception) -> Object } -> self
|
|
767
|
+
|
|
768
|
+
# An +and_fail+ or +and_skip+ code the action could never end with is a
|
|
769
|
+
# broken spec, not a failed expectation, so it raises rather than not
|
|
770
|
+
# matching.
|
|
771
|
+
def ensure_code_declared!: () -> void
|
|
772
|
+
|
|
773
|
+
# Run the action, keeping its result or the error it raised.
|
|
774
|
+
def perform: () -> void
|
|
775
|
+
|
|
776
|
+
def primary_match?: () -> bool?
|
|
777
|
+
|
|
778
|
+
def succeeded?: () -> bool?
|
|
779
|
+
|
|
780
|
+
def failed?: () -> bool?
|
|
781
|
+
|
|
782
|
+
def skipped?: () -> bool?
|
|
783
|
+
|
|
784
|
+
def raised?: () -> bool
|
|
785
|
+
|
|
786
|
+
# _@param_ `actual`
|
|
787
|
+
def message_matches?: (String? actual) -> bool
|
|
788
|
+
|
|
789
|
+
def block_match?: () -> bool
|
|
790
|
+
|
|
791
|
+
def expectation_phrase: () -> String?
|
|
792
|
+
|
|
793
|
+
def code_suffix: () -> String
|
|
794
|
+
|
|
795
|
+
def message_suffix: () -> String
|
|
796
|
+
|
|
797
|
+
def actual_phrase: () -> String
|
|
798
|
+
|
|
799
|
+
def formatted_backtrace: () -> String
|
|
800
|
+
|
|
801
|
+
def short_backtrace?: () -> bool
|
|
802
|
+
|
|
803
|
+
def backtrace_qty: () -> Integer
|
|
804
|
+
end
|
|
805
|
+
end
|
|
806
|
+
|
|
807
|
+
# Namespace for the concrete step types an action's pipeline is built from
|
|
808
|
+
# (decision D3): {Steps::Method} now, {Steps::Action} and {Steps::Case} later.
|
|
809
|
+
module Steps
|
|
810
|
+
# Build the right step type from a +step+ declaration's target, inferring
|
|
811
|
+
# the type from the argument (decision D2): a Symbol/String becomes a
|
|
812
|
+
# {Steps::Method}, an action +Class+ becomes a {Steps::Action}. The case
|
|
813
|
+
# block type ({Steps::Case}) joins in a later slice.
|
|
814
|
+
#
|
|
815
|
+
# _@param_ `target` — the step target
|
|
816
|
+
#
|
|
817
|
+
# _@param_ `options` — step options (e.g. +:if+, +:unless+, and for action steps +:input+ / +:expose+)
|
|
818
|
+
#
|
|
819
|
+
# _@return_ — the constructed step
|
|
820
|
+
def self.build: ((Symbol | String | singleton(Actionable::Action)) target, **Object options) -> Steps::Base
|
|
821
|
+
|
|
822
|
+
# Shared behavior for every step: a +name+, an +options+ hash, value
|
|
823
|
+
# identity for Set deduplication (keyed by +[type, name]+, decision D2),
|
|
824
|
+
# and the +:if+ / +:unless+ skip logic. Subclasses implement {#call}.
|
|
825
|
+
#
|
|
826
|
+
# @abstract Subclass and implement {#call}.
|
|
827
|
+
class Base
|
|
828
|
+
# _@param_ `name` — the step name (see {#name})
|
|
829
|
+
#
|
|
830
|
+
# _@param_ `options` — step options
|
|
831
|
+
def initialize: (Object name, **Object options) -> void
|
|
832
|
+
|
|
833
|
+
# Run the step against an action instance.
|
|
834
|
+
#
|
|
835
|
+
# _@param_ `_instance` — the action instance the step runs against
|
|
836
|
+
def call: (Object _instance) -> void
|
|
837
|
+
|
|
838
|
+
# Instance method names this step needs the action to implement, used by
|
|
839
|
+
# the run-start definition guard (decision D18). None for the base; concrete
|
|
840
|
+
# types that call instance methods override this.
|
|
841
|
+
def required_methods: () -> ::Array[Symbol]
|
|
842
|
+
|
|
843
|
+
# Instance methods whose bodies run as this step, for the recording-verb
|
|
844
|
+
# position check (owner ruling R5): not guards, not a case step's value
|
|
845
|
+
# source. None for the base; a nested action is checked on its own run.
|
|
846
|
+
def body_methods: () -> ::Array[Symbol]
|
|
847
|
+
|
|
848
|
+
# _@return_ — a short tag identifying the step type, for introspection
|
|
849
|
+
# (+Actionable::Action.describe+). Concrete types override it.
|
|
850
|
+
def kind: () -> Symbol
|
|
851
|
+
|
|
852
|
+
# Whether this step should be skipped for the given instance, per its
|
|
853
|
+
# +:if+ / +:unless+ guards. A guard is a Symbol naming a (possibly
|
|
854
|
+
# private) instance method, or a callable invoked with the instance.
|
|
855
|
+
# When both guards are present, either one asking to skip wins.
|
|
856
|
+
#
|
|
857
|
+
# _@param_ `instance` — the action instance to evaluate guards against
|
|
858
|
+
def skip?: (Object _instance) -> bool
|
|
859
|
+
|
|
860
|
+
# Value identity: same concrete class AND same name. This keys steps in
|
|
861
|
+
# the per-action Set so a redeclared step dedups (decision D2).
|
|
862
|
+
#
|
|
863
|
+
# _@param_ `other`
|
|
864
|
+
def ==: (Object other) -> bool
|
|
865
|
+
|
|
866
|
+
# _@return_ — consistent with {#==}, so steps work as Set members
|
|
867
|
+
def hash: () -> Integer
|
|
868
|
+
|
|
869
|
+
# Resolve a guard against the instance. A Symbol/String names an instance
|
|
870
|
+
# method (called via +__send__+, so private predicates work); anything
|
|
871
|
+
# else is treated as a callable receiving the instance.
|
|
872
|
+
#
|
|
873
|
+
# _@param_ `guard`
|
|
874
|
+
#
|
|
875
|
+
# _@param_ `instance`
|
|
876
|
+
def evaluate_guard: ((Symbol | String | untyped) guard, Object _instance) -> bool
|
|
877
|
+
|
|
878
|
+
# _@return_ — the step's name — a Symbol for a {Steps::Method},
|
|
879
|
+
# an action Class for a {Steps::Action}
|
|
880
|
+
attr_reader name: Object
|
|
881
|
+
|
|
882
|
+
# _@return_ — the declared step options (e.g. +:if+,
|
|
883
|
+
# +:unless+, and step-type-specific keys)
|
|
884
|
+
attr_reader options: ::Hash[Symbol, Object]
|
|
885
|
+
end
|
|
886
|
+
|
|
887
|
+
# A step that branches to one of several targets based on a value
|
|
888
|
+
# (decision D3, declared with +case_step+). The switch value is read from
|
|
889
|
+
# the action via the named method; each branch's target is an ordinary step
|
|
890
|
+
# (a method or a nested action), so branches compose like any other step.
|
|
891
|
+
#
|
|
892
|
+
# case_step :status do
|
|
893
|
+
# on 'active', :handle_active
|
|
894
|
+
# on %w[trial grace], :handle_trial # Array membership
|
|
895
|
+
# on(/\A4\d\d\z/, :handle_client_err # Regexp
|
|
896
|
+
# default :handle_unknown
|
|
897
|
+
# end
|
|
898
|
+
class Case < Actionable::Steps::Base
|
|
899
|
+
# Build a case step from a +case_step+ declaration.
|
|
900
|
+
#
|
|
901
|
+
# _@param_ `value_source` — the method whose value is switched on
|
|
902
|
+
#
|
|
903
|
+
# _@param_ `options` — step options (e.g. +:if+, +:unless+)
|
|
904
|
+
def self.define: ((Symbol | String) value_source, **Object options) -> Case
|
|
905
|
+
|
|
906
|
+
# _@param_ `value_source` — the method whose value is switched on
|
|
907
|
+
#
|
|
908
|
+
# _@param_ `branches` — ordered [match, step] pairs
|
|
909
|
+
#
|
|
910
|
+
# _@param_ `default_step` — the fallback step
|
|
911
|
+
#
|
|
912
|
+
# _@param_ `options` — step options
|
|
913
|
+
def initialize: (
|
|
914
|
+
(Symbol | String) value_source,
|
|
915
|
+
::Array[[Object, Steps::Base]] branches,
|
|
916
|
+
Steps::Base? default_step,
|
|
917
|
+
**Object options
|
|
918
|
+
) -> void
|
|
919
|
+
|
|
920
|
+
# Evaluate the switch value and run the first matching branch (or the
|
|
921
|
+
# default). No-op when nothing matches and no default was declared.
|
|
922
|
+
#
|
|
923
|
+
# The branch is chosen by value alone; its +:if+ / +:unless+ guards then
|
|
924
|
+
# decide whether its target runs, exactly as for a plain step. A matched
|
|
925
|
+
# branch whose guard opts out is a no-op: it does not fall through to a
|
|
926
|
+
# later branch or to the default.
|
|
927
|
+
#
|
|
928
|
+
# _@param_ `instance`
|
|
929
|
+
def call: (Actionable::Action _instance) -> void
|
|
930
|
+
|
|
931
|
+
# _@return_ — the switch-value method plus every branch
|
|
932
|
+
# target's required methods
|
|
933
|
+
def required_methods: () -> ::Array[Symbol]
|
|
934
|
+
|
|
935
|
+
# _@return_ — every branch target's body methods (never the
|
|
936
|
+
# switch-value method, which only returns the value)
|
|
937
|
+
def body_methods: () -> ::Array[Symbol]
|
|
938
|
+
|
|
939
|
+
def kind: () -> Symbol
|
|
940
|
+
|
|
941
|
+
# _@return_ — every branch's step, the default included
|
|
942
|
+
def branch_steps: () -> ::Array[Steps::Base]
|
|
943
|
+
|
|
944
|
+
# _@param_ `value` — the switch value
|
|
945
|
+
#
|
|
946
|
+
# _@return_ — the first matching branch's step
|
|
947
|
+
def match: (Object value) -> Steps::Base?
|
|
948
|
+
|
|
949
|
+
# Collects +on+/+default+ branch declarations inside a +case_step+ block.
|
|
950
|
+
class Builder
|
|
951
|
+
def initialize: () -> void
|
|
952
|
+
|
|
953
|
+
# Register a branch. +value+ matches the switch value by +==+, by
|
|
954
|
+
# +Regexp#match?+ when it is a Regexp, or by membership when it is an
|
|
955
|
+
# Array. +target+ is any step target (method name or nested action).
|
|
956
|
+
#
|
|
957
|
+
# _@param_ `value`
|
|
958
|
+
#
|
|
959
|
+
# _@param_ `target`
|
|
960
|
+
#
|
|
961
|
+
# _@param_ `options` — step options for the target (+:if+ / +:unless+ guard the branch: a matched branch whose guard opts out does nothing)
|
|
962
|
+
def on: ((Object | Regexp | ::Array[untyped]) value, (Symbol | String | singleton(Actionable::Action)) target, **Object options) -> void
|
|
963
|
+
|
|
964
|
+
# Register the fallback branch, run when no +on+ branch matches.
|
|
965
|
+
#
|
|
966
|
+
# _@param_ `target`
|
|
967
|
+
#
|
|
968
|
+
# _@param_ `options` — step options for the target
|
|
969
|
+
def default: ((Symbol | String | singleton(Actionable::Action)) target, **Object options) -> void
|
|
970
|
+
|
|
971
|
+
# _@return_ — ordered [match, step] pairs
|
|
972
|
+
attr_reader branches: ::Array[[Object, Steps::Base]]
|
|
973
|
+
|
|
974
|
+
# _@return_ — the fallback step, if +default+ was declared
|
|
975
|
+
attr_reader default_step: Steps::Base?
|
|
976
|
+
end
|
|
977
|
+
end
|
|
978
|
+
|
|
979
|
+
# A step that runs another action as part of this one (decision D3,
|
|
980
|
+
# triggered by an action +Class+ step argument).
|
|
981
|
+
#
|
|
982
|
+
# +:input+ threads named parent instance variables into the child's +.run+;
|
|
983
|
+
# +:expose+ limits which of the child's outputs are absorbed back as parent
|
|
984
|
+
# instance variables (default: all). A child failure fails the parent with
|
|
985
|
+
# the child's code, message, errors and metadata, and halts the remaining
|
|
986
|
+
# steps.
|
|
987
|
+
class Action < Actionable::Steps::Base
|
|
988
|
+
# Run the nested action, threading input in and absorbing output back.
|
|
989
|
+
#
|
|
990
|
+
# _@param_ `instance` — the parent action instance
|
|
991
|
+
def call: (Actionable::Action _instance) -> void
|
|
992
|
+
|
|
993
|
+
def kind: () -> Symbol
|
|
994
|
+
|
|
995
|
+
# _@param_ `instance`
|
|
996
|
+
#
|
|
997
|
+
# _@return_ — +{ name => @name }+ for each +:input+ name
|
|
998
|
+
def input_kwargs: (Actionable::Action _instance) -> ::Hash[Symbol, Object]
|
|
999
|
+
|
|
1000
|
+
# Copy the child's exposed output fields onto the parent as instance
|
|
1001
|
+
# variables, so the parent's later steps and output capture see them.
|
|
1002
|
+
# Only fields the child actually set are copied: a field it left nil or
|
|
1003
|
+
# at its default must not overwrite the parent's value.
|
|
1004
|
+
#
|
|
1005
|
+
# _@param_ `instance`
|
|
1006
|
+
#
|
|
1007
|
+
# _@param_ `child`
|
|
1008
|
+
def absorb: (Actionable::Action _instance, Result child) -> void
|
|
1009
|
+
|
|
1010
|
+
# _@param_ `output`
|
|
1011
|
+
#
|
|
1012
|
+
# _@return_ — the child output as a plain hash
|
|
1013
|
+
def output_hash: ((FieldStruct::Base | ::Hash[untyped, untyped] | Object) output) -> ::Hash[Symbol, Object]
|
|
1014
|
+
end
|
|
1015
|
+
|
|
1016
|
+
# A step that calls an instance method on the action (decision D3,
|
|
1017
|
+
# triggered by a Symbol/String step argument).
|
|
1018
|
+
class Method < Actionable::Steps::Base
|
|
1019
|
+
# _@param_ `name` — the instance method to call; normalized to a Symbol
|
|
1020
|
+
#
|
|
1021
|
+
# _@param_ `options` — step options (e.g. +:if+, +:unless+)
|
|
1022
|
+
def initialize: ((Symbol | String) name, **Object options) -> void
|
|
1023
|
+
|
|
1024
|
+
# Invoke the named method on the instance.
|
|
1025
|
+
#
|
|
1026
|
+
# _@param_ `instance` — the action instance
|
|
1027
|
+
#
|
|
1028
|
+
# _@return_ — the method's return value
|
|
1029
|
+
def call: (Object _instance) -> Object
|
|
1030
|
+
|
|
1031
|
+
# _@return_ — the instance method this step calls
|
|
1032
|
+
def required_methods: () -> ::Array[Symbol]
|
|
1033
|
+
|
|
1034
|
+
# _@return_ — the instance method whose body is this step
|
|
1035
|
+
def body_methods: () -> ::Array[Symbol]
|
|
1036
|
+
|
|
1037
|
+
def kind: () -> Symbol
|
|
1038
|
+
end
|
|
1039
|
+
end
|
|
1040
|
+
|
|
1041
|
+
# Base class for service objects. Subclass it, declare an ordered list of
|
|
1042
|
+
# steps with {.step}, implement each step as an instance method, and run it
|
|
1043
|
+
# with {.run} (decisions D1/D2).
|
|
1044
|
+
#
|
|
1045
|
+
# class Greet < Actionable::Action
|
|
1046
|
+
# step :build_message
|
|
1047
|
+
# def build_message = @message = 'hello'
|
|
1048
|
+
# end
|
|
1049
|
+
#
|
|
1050
|
+
# Greet.run # => #<Actionable::Success ...>
|
|
1051
|
+
class Action
|
|
1052
|
+
extend Actionable::Rails::Transactions
|
|
1053
|
+
extend Actionable::Job::RetryMetadata
|
|
1054
|
+
GEM_FAILURE_CODES: ::Array[Symbol]
|
|
1055
|
+
CORE_METHOD_OWNERS: ::Array[Module]
|
|
1056
|
+
CODE_FORMAT: Regexp
|
|
1057
|
+
|
|
1058
|
+
# The ordered, deduplicated set of steps declared on this action.
|
|
1059
|
+
# A +Set+ keyed by step identity ([type, name]) so a redeclared step
|
|
1060
|
+
# collapses to one entry; insertion order is preserved (decision D2).
|
|
1061
|
+
# The last declaration of a step wins and keeps the first one's position
|
|
1062
|
+
# (see {.step}).
|
|
1063
|
+
def self.steps: () -> ::Set[Steps::Base]
|
|
1064
|
+
|
|
1065
|
+
# Declare the action's typed output schema, or read it back.
|
|
1066
|
+
#
|
|
1067
|
+
# With a block, builds an anonymous +FieldStruct::Base+ subclass from the
|
|
1068
|
+
# FieldStruct DSL (+required+/+optional+/…) and stores it as this action's
|
|
1069
|
+
# output schema (decision D6). At the end of a run the {Runner} captures
|
|
1070
|
+
# the instance variables whose names match declared fields, coerces them
|
|
1071
|
+
# through the schema, and assigns the struct to +result.output+. Field names
|
|
1072
|
+
# in {RESERVED_OUTPUT_FIELDS} raise a {DefinitionError}.
|
|
1073
|
+
#
|
|
1074
|
+
# output do
|
|
1075
|
+
# required :invoice, Invoice
|
|
1076
|
+
# optional :receipt, Receipt
|
|
1077
|
+
# end
|
|
1078
|
+
#
|
|
1079
|
+
# _@return_ — the output schema, or +nil+ when
|
|
1080
|
+
# none has been declared (the free-form path)
|
|
1081
|
+
def self.output: () -> singleton(FieldStruct::Base)?
|
|
1082
|
+
|
|
1083
|
+
# Declare the action's typed input schema (decision D7). Builds an
|
|
1084
|
+
# anonymous +FieldStruct::Base+ subclass from the FieldStruct DSL; +.run+
|
|
1085
|
+
# then coerces its arguments into this schema, validates them, and exposes
|
|
1086
|
+
# the struct to steps via the instance's +input+ reader. Declaring input
|
|
1087
|
+
# takes over the constructor role — steps read +input.field+ rather than
|
|
1088
|
+
# capturing positional/keyword args in a custom +initialize+.
|
|
1089
|
+
#
|
|
1090
|
+
# input do
|
|
1091
|
+
# required :amount, :big_decimal
|
|
1092
|
+
# optional :name, :string
|
|
1093
|
+
# end
|
|
1094
|
+
#
|
|
1095
|
+
# Called without a block it reads the declared schema, as {.output} does.
|
|
1096
|
+
#
|
|
1097
|
+
# _@return_ — the input schema, or +nil+ when
|
|
1098
|
+
# none has been declared
|
|
1099
|
+
def self.input: () -> singleton(FieldStruct::Base)?
|
|
1100
|
+
|
|
1101
|
+
# Declare a *discriminated* input schema (decision D19): pick the input
|
|
1102
|
+
# FieldStruct at run time from a discriminator value, instead of a single
|
|
1103
|
+
# fixed schema. The block declares branches with +on(value, Schema)+ and an
|
|
1104
|
+
# optional +default(Schema)+; +.run+ reads the discriminator from its
|
|
1105
|
+
# keyword arguments, chooses the schema, then coerces and validates exactly
|
|
1106
|
+
# like a fixed +input+. An unmatched value with no +default+ short-circuits
|
|
1107
|
+
# to +Failure(:invalid_input)+.
|
|
1108
|
+
#
|
|
1109
|
+
# input_for :event_type do
|
|
1110
|
+
# on 'sale', SaleShape
|
|
1111
|
+
# on 'refund', RefundShape
|
|
1112
|
+
# default UnknownShape
|
|
1113
|
+
# end
|
|
1114
|
+
#
|
|
1115
|
+
# _@param_ `discriminator` — the kwarg whose value selects the schema
|
|
1116
|
+
#
|
|
1117
|
+
# _@return_ — the stored dispatch
|
|
1118
|
+
def self.input_for: ((Symbol | String) discriminator) -> InputDispatch
|
|
1119
|
+
|
|
1120
|
+
# Declare one failure code this action can end with, and the metadata a
|
|
1121
|
+
# failure with that code carries (decision D20).
|
|
1122
|
+
#
|
|
1123
|
+
# The metadata is neutral: arbitrary keywords the gem stores, attaches to
|
|
1124
|
+
# the {Failure} as +result.metadata+, and shows in {.describe}, but never
|
|
1125
|
+
# interprets. A host app gives the keys their meaning (say, +kind:+ for
|
|
1126
|
+
# how an adapter answers the failure). +message+ is the default message
|
|
1127
|
+
# for a +fail+ that gives none.
|
|
1128
|
+
#
|
|
1129
|
+
# fails :refused, 'the card was refused', kind: :rejected
|
|
1130
|
+
# fails :not_found, kind: :not_found
|
|
1131
|
+
#
|
|
1132
|
+
# Declarations are inherited and a subclass can add to them; redeclaring a
|
|
1133
|
+
# code replaces its message and metadata. Once an action declares any
|
|
1134
|
+
# code, failing with an undeclared one raises a {DefinitionError} at the
|
|
1135
|
+
# +fail+ call; the {GEM_FAILURE_CODES} are exempt.
|
|
1136
|
+
#
|
|
1137
|
+
# A block declares the output a failure with this code carries, in the
|
|
1138
|
+
# FieldStruct DSL of {.output} (decision D23); see {.failure_output_schema}.
|
|
1139
|
+
#
|
|
1140
|
+
# _@param_ `code` — the failure code
|
|
1141
|
+
#
|
|
1142
|
+
# _@param_ `message` — the default message for this code
|
|
1143
|
+
#
|
|
1144
|
+
# _@param_ `metadata` — per-code metadata, passed through
|
|
1145
|
+
#
|
|
1146
|
+
# _@return_ — {.declared_failures}
|
|
1147
|
+
def self.fails: ((Symbol | String) code, ?String? message, **Object metadata) -> ::Hash[Symbol, ::Hash[Symbol, Object]]
|
|
1148
|
+
|
|
1149
|
+
# The output schema declared for failure +code+ with a {.fails} block, or
|
|
1150
|
+
# +nil+ when the code has none (decision D23).
|
|
1151
|
+
#
|
|
1152
|
+
# _@param_ `code`
|
|
1153
|
+
def self.failure_output_schema: ((Symbol | String) code) -> singleton(FieldStruct::Base)?
|
|
1154
|
+
|
|
1155
|
+
# The failure codes this action declares with {.fails}, each mapped to its
|
|
1156
|
+
# metadata (decision D20). Empty when the action declares none.
|
|
1157
|
+
def self.declared_failures: () -> ::Hash[Symbol, ::Hash[Symbol, Object]]
|
|
1158
|
+
|
|
1159
|
+
# Whether this action may end with a failure of +code+ (decision D20):
|
|
1160
|
+
# always when it declares no failures, otherwise when it declares +code+
|
|
1161
|
+
# or +code+ is one of the {GEM_FAILURE_CODES}. The check behind the
|
|
1162
|
+
# strictness guard, the RSpec stubs and +and_fail+.
|
|
1163
|
+
#
|
|
1164
|
+
# _@param_ `code`
|
|
1165
|
+
def self.allows_failure_code?: ((Symbol | String) code) -> bool
|
|
1166
|
+
|
|
1167
|
+
# Every {.fails} declaration, keyed by code, as +{message:, metadata:}+.
|
|
1168
|
+
def self.failure_declarations: () -> ::Hash[Symbol, ::Hash[Symbol, Object]]
|
|
1169
|
+
|
|
1170
|
+
# Declare one skip code this action can end with, and the metadata a skip
|
|
1171
|
+
# with that code carries (decision D21). The skip counterpart of {.fails}.
|
|
1172
|
+
#
|
|
1173
|
+
# The metadata is neutral: arbitrary keywords the gem stores, attaches to
|
|
1174
|
+
# the {Skipped} as +result.metadata+, and shows in {.describe}, but never
|
|
1175
|
+
# interprets. +message+ is the default message for a +skip+ that gives
|
|
1176
|
+
# none.
|
|
1177
|
+
#
|
|
1178
|
+
# skips :not_ready, 'the field is not created yet', retry: true
|
|
1179
|
+
# skips :already_synced
|
|
1180
|
+
#
|
|
1181
|
+
# Declarations are inherited and a subclass can add to them; redeclaring a
|
|
1182
|
+
# code replaces its message and metadata. Once an action declares any
|
|
1183
|
+
# skip, skipping with an undeclared code raises a {DefinitionError} at the
|
|
1184
|
+
# +skip+ call, and that includes +skip+'s implicit default +:skipped+.
|
|
1185
|
+
#
|
|
1186
|
+
# A block declares the output a skip with this code carries, in the
|
|
1187
|
+
# FieldStruct DSL of {.output} (decision D23); see {.skip_output_schema}.
|
|
1188
|
+
#
|
|
1189
|
+
# skips :already_active do
|
|
1190
|
+
# required :id, :integer
|
|
1191
|
+
# end
|
|
1192
|
+
#
|
|
1193
|
+
# _@param_ `code` — the skip code
|
|
1194
|
+
#
|
|
1195
|
+
# _@param_ `message` — the default message for this code
|
|
1196
|
+
#
|
|
1197
|
+
# _@param_ `metadata` — per-code metadata, passed through
|
|
1198
|
+
#
|
|
1199
|
+
# _@return_ — {.declared_skips}
|
|
1200
|
+
def self.skips: ((Symbol | String) code, ?String? message, **Object metadata) -> ::Hash[Symbol, ::Hash[Symbol, Object]]
|
|
1201
|
+
|
|
1202
|
+
# The output schema declared for skip +code+ with a {.skips} block, or
|
|
1203
|
+
# +nil+ when the code has none (decision D23).
|
|
1204
|
+
#
|
|
1205
|
+
# _@param_ `code`
|
|
1206
|
+
def self.skip_output_schema: ((Symbol | String) code) -> singleton(FieldStruct::Base)?
|
|
1207
|
+
|
|
1208
|
+
# Turn typed outcomes on (or off) for this action and its descendants
|
|
1209
|
+
# (decision D23). With it on, a skip or failure whose code declares no
|
|
1210
|
+
# output schema carries an empty output instead of a best-effort capture,
|
|
1211
|
+
# and {.run} returns the action's own result classes: +Success+,
|
|
1212
|
+
# +Failure+ and +Skipped+ defined inside it, each a subclass of the gem
|
|
1213
|
+
# class (or of the parent action's own, for a subclass).
|
|
1214
|
+
#
|
|
1215
|
+
# _@param_ `enabled`
|
|
1216
|
+
#
|
|
1217
|
+
# _@return_ — the new setting
|
|
1218
|
+
def self.typed_outcomes: (?enabled: bool) -> bool
|
|
1219
|
+
|
|
1220
|
+
# _@return_ — whether typed outcomes are on (decision D23)
|
|
1221
|
+
def self.typed_outcomes?: () -> bool
|
|
1222
|
+
|
|
1223
|
+
# The skip codes this action declares with {.skips}, each mapped to its
|
|
1224
|
+
# metadata (decision D21). Empty when the action declares none.
|
|
1225
|
+
def self.declared_skips: () -> ::Hash[Symbol, ::Hash[Symbol, Object]]
|
|
1226
|
+
|
|
1227
|
+
# Whether this action may end with a skip of +code+ (decision D21): always
|
|
1228
|
+
# when it declares no skips, otherwise only when it declares +code+. The
|
|
1229
|
+
# gem emits no skip codes of its own, so nothing is exempt, +skip+'s
|
|
1230
|
+
# default +:skipped+ included. The check behind the strictness guard, the
|
|
1231
|
+
# RSpec skip stubs and +and_skip+.
|
|
1232
|
+
#
|
|
1233
|
+
# _@param_ `code`
|
|
1234
|
+
def self.allows_skip_code?: ((Symbol | String) code) -> bool
|
|
1235
|
+
|
|
1236
|
+
# Every {.skips} declaration, keyed by code, as +{message:, metadata:}+
|
|
1237
|
+
# (decision D21).
|
|
1238
|
+
def self.skip_declarations: () -> ::Hash[Symbol, ::Hash[Symbol, Object]]
|
|
1239
|
+
|
|
1240
|
+
# Declare a step. The step type is inferred from +target+ (a Symbol or
|
|
1241
|
+
# String names an instance method — a {Steps::Method}).
|
|
1242
|
+
#
|
|
1243
|
+
# Redeclaring a step of the same type and name (here or in a subclass)
|
|
1244
|
+
# replaces the earlier declaration's options in place: the last
|
|
1245
|
+
# declaration wins and keeps the original position. A subclass's
|
|
1246
|
+
# override never changes the parent's steps.
|
|
1247
|
+
#
|
|
1248
|
+
# _@param_ `target` — the step target
|
|
1249
|
+
#
|
|
1250
|
+
# _@param_ `options` — step options (e.g. +:if+, +:unless+)
|
|
1251
|
+
#
|
|
1252
|
+
# _@return_ — the updated step set
|
|
1253
|
+
def self.step: ((Symbol | String) target, **Object options) -> ::Set[Steps::Base]
|
|
1254
|
+
|
|
1255
|
+
# Lifecycle hooks that run after the main steps when the run succeeds.
|
|
1256
|
+
def self.success_hooks: () -> ::Set[Steps::Base]
|
|
1257
|
+
|
|
1258
|
+
# Lifecycle hooks that run after the main steps when the run fails.
|
|
1259
|
+
def self.failure_hooks: () -> ::Set[Steps::Base]
|
|
1260
|
+
|
|
1261
|
+
# Lifecycle hooks that run after the main steps when the run was skipped.
|
|
1262
|
+
def self.skip_hooks: () -> ::Set[Steps::Base]
|
|
1263
|
+
|
|
1264
|
+
# Lifecycle hooks that run after the main steps regardless of outcome.
|
|
1265
|
+
def self.always_hooks: () -> ::Set[Steps::Base]
|
|
1266
|
+
|
|
1267
|
+
# Declare a hook that runs at the end of a run iff it succeeded (D2).
|
|
1268
|
+
#
|
|
1269
|
+
# _@param_ `target` — the instance method to run
|
|
1270
|
+
#
|
|
1271
|
+
# _@param_ `options` — step options (e.g. +:if+)
|
|
1272
|
+
def self.on_success: ((Symbol | String) target, **Object options) -> ::Set[Steps::Base]
|
|
1273
|
+
|
|
1274
|
+
# Declare a hook that runs at the end of a run iff it failed (D2).
|
|
1275
|
+
#
|
|
1276
|
+
# _@param_ `target` — the instance method to run
|
|
1277
|
+
#
|
|
1278
|
+
# _@param_ `options` — step options (e.g. +:if+)
|
|
1279
|
+
def self.on_failure: ((Symbol | String) target, **Object options) -> ::Set[Steps::Base]
|
|
1280
|
+
|
|
1281
|
+
# Declare a hook that runs at the end of a run iff it was skipped (D17).
|
|
1282
|
+
#
|
|
1283
|
+
# _@param_ `target` — the instance method to run
|
|
1284
|
+
#
|
|
1285
|
+
# _@param_ `options` — step options (e.g. +:if+)
|
|
1286
|
+
def self.on_skip: ((Symbol | String) target, **Object options) -> ::Set[Steps::Base]
|
|
1287
|
+
|
|
1288
|
+
# Declare a hook that runs at the end of a run regardless of outcome (D2).
|
|
1289
|
+
#
|
|
1290
|
+
# _@param_ `target` — the instance method to run
|
|
1291
|
+
#
|
|
1292
|
+
# _@param_ `options` — step options (e.g. +:if+)
|
|
1293
|
+
def self.always: ((Symbol | String) target, **Object options) -> ::Set[Steps::Base]
|
|
1294
|
+
|
|
1295
|
+
# Declare a branching step (decision D3). The switch value is read from
|
|
1296
|
+
# +value_source+ (an instance method); the block declares branches with
|
|
1297
|
+
# +on(value, target)+ and an optional +default(target)+.
|
|
1298
|
+
#
|
|
1299
|
+
# case_step :status do
|
|
1300
|
+
# on 'active', :handle_active
|
|
1301
|
+
# default :handle_unknown
|
|
1302
|
+
# end
|
|
1303
|
+
#
|
|
1304
|
+
# _@param_ `value_source` — the method whose value is switched on
|
|
1305
|
+
#
|
|
1306
|
+
# _@param_ `options` — step options (e.g. +:if+, +:unless+)
|
|
1307
|
+
#
|
|
1308
|
+
# _@return_ — the updated step set
|
|
1309
|
+
def self.case_step: ((Symbol | String) value_source, **Object options) -> ::Set[Steps::Base]
|
|
1310
|
+
|
|
1311
|
+
# Enable or read the action's measurement mode (decisions D10/D19). +:all+
|
|
1312
|
+
# records an execution {History}; +:none+ (the default) records nothing for
|
|
1313
|
+
# zero overhead; +:sampled+ records on a fraction of runs (+rate:+ between 0
|
|
1314
|
+
# and 1) for low-overhead production observability. A measuring run cascades
|
|
1315
|
+
# into the nested actions it invokes regardless of their own setting.
|
|
1316
|
+
#
|
|
1317
|
+
# measure :sampled, rate: 0.1 # record ~10% of runs
|
|
1318
|
+
#
|
|
1319
|
+
# _@param_ `mode` — the mode to set; omit to read
|
|
1320
|
+
#
|
|
1321
|
+
# _@param_ `rate` — required for +:sampled+ — the probability (0..1) that a given run is measured
|
|
1322
|
+
#
|
|
1323
|
+
# _@return_ — the resolved mode
|
|
1324
|
+
def self.measure: (?Symbol? mode, ?rate: Numeric?) -> Symbol
|
|
1325
|
+
|
|
1326
|
+
# _@return_ — whether this action always records history
|
|
1327
|
+
def self.measure_all?: () -> bool
|
|
1328
|
+
|
|
1329
|
+
# _@return_ — whether this action samples history
|
|
1330
|
+
def self.measure_sampled?: () -> bool
|
|
1331
|
+
|
|
1332
|
+
# The per-run sampling decision: true with probability {.measure_rate} when
|
|
1333
|
+
# in +:sampled+ mode, false otherwise. Rolled once per run by the {Runner}.
|
|
1334
|
+
def self.measure_sample_hit?: () -> bool
|
|
1335
|
+
|
|
1336
|
+
# A structured, at-a-glance summary of the action — its name, input/output
|
|
1337
|
+
# field metadata, ordered steps (with type), declared failures and skips
|
|
1338
|
+
# (each code's message and metadata, decisions D20/D21), lifecycle hooks, measurement mode,
|
|
1339
|
+
# and transaction config (decision D18). Lets humans and agents
|
|
1340
|
+
# understand an action without reading its source.
|
|
1341
|
+
def self.describe: () -> ::Hash[Symbol, Object]
|
|
1342
|
+
|
|
1343
|
+
# A human/agent-readable rendering of {.describe} — the same information as
|
|
1344
|
+
# a multi-line summary instead of a Hash (decision D18). The input/output
|
|
1345
|
+
# field lines reuse FieldStruct's own +Metadata#describe+ (type,
|
|
1346
|
+
# required-ness, and the options each field's type accepts); declared
|
|
1347
|
+
# failures, declared skips and hook sections are listed only when there are any.
|
|
1348
|
+
#
|
|
1349
|
+
# Greet.describe_text
|
|
1350
|
+
# # => "Greet (measure: none)\n Input: (free-form)\n ..."
|
|
1351
|
+
def self.describe_text: () -> String
|
|
1352
|
+
|
|
1353
|
+
# Run the action. With a declared input schema, coerce the arguments
|
|
1354
|
+
# (keyword args, or a single input-schema instance) into the input struct
|
|
1355
|
+
# and validate it — a required input that is missing or won't coerce
|
|
1356
|
+
# short-circuits to a +Failure(:invalid_input)+ without running any steps
|
|
1357
|
+
# (decision D7). Without an input schema, the arguments are forwarded
|
|
1358
|
+
# verbatim to the action's constructor (the free-form path).
|
|
1359
|
+
#
|
|
1360
|
+
# Under typed outcomes the result is an instance of the action's own
|
|
1361
|
+
# result class for its outcome (decision D23).
|
|
1362
|
+
#
|
|
1363
|
+
# _@param_ `args` — positional arguments: a single input-schema instance, or (free-form) whatever the constructor takes
|
|
1364
|
+
#
|
|
1365
|
+
# _@param_ `kwargs` — keyword arguments: the input fields, or (free-form) whatever the constructor takes
|
|
1366
|
+
#
|
|
1367
|
+
# _@return_ — the run's {Success} or {Failure}
|
|
1368
|
+
def self.run: (*Object args, **Object kwargs) -> Result
|
|
1369
|
+
|
|
1370
|
+
# Run the action once per item in +enumerable+, collecting the per-item
|
|
1371
|
+
# results into a {BatchResult} (decision D19). The single most common
|
|
1372
|
+
# batch/ingest shape: each item runs independently (its own {.run}, its own
|
|
1373
|
+
# outcome), so one item's failure never affects another, and the aggregate
|
|
1374
|
+
# is inspectable via {BatchResult#all_ok?} / +#any_failure?+ / +#partial?+.
|
|
1375
|
+
#
|
|
1376
|
+
# The optional block maps each item to +.run+ arguments: return a Hash for
|
|
1377
|
+
# keyword args, an Array for positional args, or any other value to pass as
|
|
1378
|
+
# a single argument. Without a block, each item is passed to +.run+
|
|
1379
|
+
# directly.
|
|
1380
|
+
#
|
|
1381
|
+
# ImportRow.run_each(rows) { |row| {row: row} }
|
|
1382
|
+
# Charge.run_each(amounts) # => each amount passed to .run
|
|
1383
|
+
#
|
|
1384
|
+
# _@param_ `enumerable` — the items to run
|
|
1385
|
+
#
|
|
1386
|
+
# _@return_ — the per-item results
|
|
1387
|
+
def self.run_each: (::Enumerable[untyped] enumerable) ?{ (Object item) -> (::Hash[untyped, untyped] | ::Array[untyped] | Object) } -> BatchResult
|
|
1388
|
+
|
|
1389
|
+
# The body of {.run}: build the input, then run the steps.
|
|
1390
|
+
#
|
|
1391
|
+
# _@param_ `args`
|
|
1392
|
+
#
|
|
1393
|
+
# _@param_ `kwargs`
|
|
1394
|
+
def self.run_untyped: (::Array[Object] args, ::Hash[Symbol, Object] kwargs) -> Result
|
|
1395
|
+
|
|
1396
|
+
# Define this action's own +Success+, +Failure+ and +Skipped+ (decision
|
|
1397
|
+
# D23), each subclassing the parent action's own when it has them, else
|
|
1398
|
+
# the gem class. Defined once; a later call is a no-op.
|
|
1399
|
+
#
|
|
1400
|
+
# _@return_ — status => result class
|
|
1401
|
+
def self.define_outcome_classes: () -> ::Hash[Symbol, singleton(Result)]
|
|
1402
|
+
|
|
1403
|
+
# _@param_ `const` — the constant to define
|
|
1404
|
+
#
|
|
1405
|
+
# _@param_ `status` — the outcome {Result#status}
|
|
1406
|
+
#
|
|
1407
|
+
# _@param_ `base` — the gem result class
|
|
1408
|
+
def self.define_outcome_class: (Symbol const, Symbol status, singleton(Result) base) -> singleton(Result)
|
|
1409
|
+
|
|
1410
|
+
# The result as this action's own result class for its outcome, under
|
|
1411
|
+
# typed outcomes (decision D23). A result already of that class, or of a
|
|
1412
|
+
# host's own result subclass, is returned as it is; a gem result, or
|
|
1413
|
+
# another action's own (a nested failure that crossed), is rebuilt with
|
|
1414
|
+
# its attributes and errors.
|
|
1415
|
+
#
|
|
1416
|
+
# _@param_ `result`
|
|
1417
|
+
def self.typed_result: (Result result) -> Result
|
|
1418
|
+
|
|
1419
|
+
# Build an output schema from a FieldStruct DSL block, refusing a field
|
|
1420
|
+
# named like a result attribute (decisions D6/D18/D23).
|
|
1421
|
+
#
|
|
1422
|
+
# _@param_ `block` — the FieldStruct field declarations
|
|
1423
|
+
def self.build_output_schema: (Proc block) -> singleton(FieldStruct::Base)
|
|
1424
|
+
|
|
1425
|
+
# One {.fails} / {.skips} declaration: the default message, the frozen
|
|
1426
|
+
# metadata, and the output schema when a block declares one (D20/D21/D23).
|
|
1427
|
+
#
|
|
1428
|
+
# _@param_ `message`
|
|
1429
|
+
#
|
|
1430
|
+
# _@param_ `metadata`
|
|
1431
|
+
#
|
|
1432
|
+
# _@param_ `block`
|
|
1433
|
+
def self.declaration: (String? message, ::Hash[Symbol, Object] metadata, Proc? block) -> ::Hash[Symbol, Object]
|
|
1434
|
+
|
|
1435
|
+
# {.describe}'s view of the declarations: each code's message and
|
|
1436
|
+
# metadata, plus its output fields when it declares an output schema.
|
|
1437
|
+
#
|
|
1438
|
+
# _@param_ `declarations`
|
|
1439
|
+
def self.describe_declarations: (::Hash[Symbol, ::Hash[Symbol, Object]] declarations) -> ::Hash[Symbol, ::Hash[Symbol, Object]]
|
|
1440
|
+
|
|
1441
|
+
# One {.describe_text} block for an input/output schema: +" (free-form)"+
|
|
1442
|
+
# when none is declared, otherwise the FieldStruct +Metadata#describe+
|
|
1443
|
+
# field lines indented one level under their +Input:+/+Output:+ heading.
|
|
1444
|
+
#
|
|
1445
|
+
# _@param_ `schema`
|
|
1446
|
+
def self.schema_summary: (singleton(FieldStruct::Base)? schema) -> String
|
|
1447
|
+
|
|
1448
|
+
# The +Failures:+ or +Skips:+ lines for {.describe_text}, one per declared
|
|
1449
|
+
# code with its metadata and default message, or none when nothing is
|
|
1450
|
+
# declared (decisions D20/D21).
|
|
1451
|
+
#
|
|
1452
|
+
# _@param_ `heading` — the section heading
|
|
1453
|
+
#
|
|
1454
|
+
# _@param_ `declarations` — code => {message:, metadata:}
|
|
1455
|
+
def self.declarations_summary: (String heading, ::Hash[Symbol, ::Hash[Symbol, Object]] declarations) -> ::Array[String]
|
|
1456
|
+
|
|
1457
|
+
# The +input+ value for {.describe}: a fixed schema's field metadata, a
|
|
1458
|
+
# discriminated-input descriptor (+discriminated_on+ + +shapes+), or +nil+
|
|
1459
|
+
# for a free-form action.
|
|
1460
|
+
def self.input_descriptor: () -> ::Hash[untyped, untyped]?
|
|
1461
|
+
|
|
1462
|
+
# The +Input:+ block for {.describe_text}: the fixed-schema field lines, a
|
|
1463
|
+
# one-line discriminated-input summary, or +" (free-form)"+.
|
|
1464
|
+
def self.input_summary: () -> String
|
|
1465
|
+
|
|
1466
|
+
# Run-start guard (decision D18): every method a step needs (method steps,
|
|
1467
|
+
# case value sources and method branch targets, hook steps) must exist on
|
|
1468
|
+
# the instance, or raise a clear {DefinitionError} rather than a deep
|
|
1469
|
+
# +NoMethodError+. This is the earliest reliable point — steps are usually
|
|
1470
|
+
# declared above the methods that implement them.
|
|
1471
|
+
#
|
|
1472
|
+
# _@param_ `instance`
|
|
1473
|
+
def self.ensure_steps_implemented!: (Action _instance) -> void
|
|
1474
|
+
|
|
1475
|
+
# First-run guard (owner ruling R5): in every step and hook method, a
|
|
1476
|
+
# non-bang recording verb (+fail+ / +fail_with+ / +skip+ / +succeed+) must be the
|
|
1477
|
+
# method's last expression, or explicitly end it, since anything after
|
|
1478
|
+
# it still runs (+VerbPosition+ has the rules). Checked once per class,
|
|
1479
|
+
# by parsing each method's source with Prism; skipped where that can't
|
|
1480
|
+
# be done (no Prism, no readable source), and not run at all while
|
|
1481
|
+
# {Actionable.verb_position_check?} is off. A failing (or skipped) check
|
|
1482
|
+
# is not cached, so it raises on every run until fixed.
|
|
1483
|
+
def self.ensure_verbs_last!: () -> void
|
|
1484
|
+
|
|
1485
|
+
# The steps and hooks whose methods {.ensure_verbs_last!} checks. The
|
|
1486
|
+
# Rails adapter adds its +after_commit+ hooks.
|
|
1487
|
+
def self.verb_checked_steps: () -> ::Array[Steps::Base]
|
|
1488
|
+
|
|
1489
|
+
# Whether +instance+ implements +method+ itself (public or private), not
|
|
1490
|
+
# merely inherits it from Ruby's core: steps are called with +__send__+,
|
|
1491
|
+
# so +step :puts+ or +always :exit+ must not reach +Kernel+.
|
|
1492
|
+
#
|
|
1493
|
+
# _@param_ `instance`
|
|
1494
|
+
#
|
|
1495
|
+
# _@param_ `method`
|
|
1496
|
+
def self.implements?: (Action _instance, Symbol method) -> bool
|
|
1497
|
+
|
|
1498
|
+
# Coerce +.run+ arguments into the input struct: pass an existing input
|
|
1499
|
+
# instance through untouched, otherwise build one from the keyword args.
|
|
1500
|
+
# With discriminated input ({.input_for}), the schema is chosen from the
|
|
1501
|
+
# discriminator value first; an unmatched value returns a +Failure+.
|
|
1502
|
+
#
|
|
1503
|
+
# _@param_ `args` — the positional arguments given to {.run}
|
|
1504
|
+
#
|
|
1505
|
+
# _@param_ `kwargs` — the keyword arguments given to {.run}
|
|
1506
|
+
def self.build_input: (::Array[Object] args, ::Hash[Symbol, Object] kwargs) -> (FieldStruct::Base | Failure)
|
|
1507
|
+
|
|
1508
|
+
# Resolve the discriminated schema from the discriminator value, then build
|
|
1509
|
+
# the input the same way {.build_input} does — or return an
|
|
1510
|
+
# +:invalid_input+ {Failure} when nothing matches and there's no default.
|
|
1511
|
+
#
|
|
1512
|
+
# _@param_ `args` — the positional arguments given to {.run}
|
|
1513
|
+
#
|
|
1514
|
+
# _@param_ `kwargs` — the keyword arguments given to {.run}
|
|
1515
|
+
def self.build_dispatched_input: (::Array[Object] args, ::Hash[Symbol, Object] kwargs) -> (FieldStruct::Base | Failure)
|
|
1516
|
+
|
|
1517
|
+
# _@param_ `value` — the unmatched discriminator value
|
|
1518
|
+
#
|
|
1519
|
+
# _@return_ — code +:invalid_input+, naming the discriminator
|
|
1520
|
+
def self.unmatched_discriminator_failure: (Object value) -> Failure
|
|
1521
|
+
|
|
1522
|
+
# _@param_ `input` — the invalid input struct
|
|
1523
|
+
#
|
|
1524
|
+
# _@return_ — code +:invalid_input+, carrying the input's errors
|
|
1525
|
+
def self.invalid_input_failure: (FieldStruct::Base input) -> Failure
|
|
1526
|
+
|
|
1527
|
+
# Add +step+ to +list+, or — when an equal step (same type and name) is
|
|
1528
|
+
# already declared — swap it in at that step's position, so the last
|
|
1529
|
+
# declaration wins (owner ruling P8; invariant 5). +list+ is this class's
|
|
1530
|
+
# own copy (see {.inherited}), so a parent's list is never touched.
|
|
1531
|
+
#
|
|
1532
|
+
# _@param_ `list`
|
|
1533
|
+
#
|
|
1534
|
+
# _@param_ `step`
|
|
1535
|
+
#
|
|
1536
|
+
# _@return_ — +list+
|
|
1537
|
+
def self.declare: (::Set[Steps::Base] list, Steps::Base step) -> ::Set[Steps::Base]
|
|
1538
|
+
|
|
1539
|
+
# Give each subclass its own copy of the inherited step set, so adding
|
|
1540
|
+
# steps to a subclass never mutates the parent's (decision D2).
|
|
1541
|
+
#
|
|
1542
|
+
# _@param_ `subclass`
|
|
1543
|
+
def self.inherited: (Class subclass) -> void
|
|
1544
|
+
|
|
1545
|
+
# Default free-form constructor: accept and ignore any arguments, so an
|
|
1546
|
+
# action with no declared input still runs via {.run}. Subclasses override
|
|
1547
|
+
# this to capture their inputs (decision D7; typed input lands in a later
|
|
1548
|
+
# slice).
|
|
1549
|
+
#
|
|
1550
|
+
# _@param_ `_args` — ignored
|
|
1551
|
+
#
|
|
1552
|
+
# _@param_ `_kwargs` — ignored
|
|
1553
|
+
def initialize: (*Object _args, **Object _kwargs) -> void
|
|
1554
|
+
|
|
1555
|
+
# Record a {Failure} as the run's result without halting — later steps
|
|
1556
|
+
# still run, and the final result reflects the most recent record (decision
|
|
1557
|
+
# D4, "last write wins").
|
|
1558
|
+
#
|
|
1559
|
+
# A code declared with {Action.fails} attaches its metadata to the failure,
|
|
1560
|
+
# and its declared message when +message+ is +nil+ (decision D20). Nothing
|
|
1561
|
+
# but +errors+ follows the message, so an error on any field name, +kind+
|
|
1562
|
+
# included, lands in +errors+.
|
|
1563
|
+
#
|
|
1564
|
+
# Under typed outcomes alone, +output:+ is reserved: it is the failure's
|
|
1565
|
+
# output, held to the schema its code declares (decision D23).
|
|
1566
|
+
#
|
|
1567
|
+
# fail!(:invalid, 'check the form', output: {form_record: record}, name: 'is blank')
|
|
1568
|
+
#
|
|
1569
|
+
# _@param_ `code` — the error code
|
|
1570
|
+
#
|
|
1571
|
+
# _@param_ `message` — human-readable description
|
|
1572
|
+
#
|
|
1573
|
+
# _@param_ `errors` — each keyword is a field => message(s) added to the result's errors collection; under typed outcomes the +output:+ keyword is the failure's output instead
|
|
1574
|
+
#
|
|
1575
|
+
# _@return_ — so a step can branch on the call
|
|
1576
|
+
def fail: (Symbol code, ?String? message, **(String | ::Array[String] | ::Hash[Symbol, Object] | FieldStruct::Base) errors) -> bool
|
|
1577
|
+
|
|
1578
|
+
# Record a {Failure} that absorbs an arbitrary FieldStruct's validation
|
|
1579
|
+
# errors, without halting (decision D19). The same machinery
|
|
1580
|
+
# +Failure(:invalid_input)+ / +Failure(:invalid_output)+ use internally,
|
|
1581
|
+
# exposed as a verb so a step can validate a side struct and fail with its
|
|
1582
|
+
# errors directly.
|
|
1583
|
+
#
|
|
1584
|
+
# def validate = fail_with(EventShape.new(payload), code: :invalid_event)
|
|
1585
|
+
#
|
|
1586
|
+
# _@param_ `source` — any object exposing a FieldStruct-style +errors+ collection (typically a +FieldStruct::Base+ instance)
|
|
1587
|
+
#
|
|
1588
|
+
# _@param_ `code` — the error code; defaults to +:invalid+
|
|
1589
|
+
#
|
|
1590
|
+
# _@param_ `message` — human-readable description
|
|
1591
|
+
#
|
|
1592
|
+
# _@param_ `output` — the failure's output, held to the schema its code declares; typed outcomes only (decision D23)
|
|
1593
|
+
#
|
|
1594
|
+
# _@return_ — so a step can branch on the call
|
|
1595
|
+
def fail_with: (
|
|
1596
|
+
untyped source,
|
|
1597
|
+
?code: Symbol,
|
|
1598
|
+
?message: String?,
|
|
1599
|
+
?output: ::Hash[Symbol, Object]?
|
|
1600
|
+
) -> bool
|
|
1601
|
+
|
|
1602
|
+
# Record a {Failure} absorbing +source+'s errors and halt the run
|
|
1603
|
+
# (decision D19). The halting companion to {#fail_with}.
|
|
1604
|
+
#
|
|
1605
|
+
# _@param_ `source` — see {#fail_with}
|
|
1606
|
+
#
|
|
1607
|
+
# _@param_ `code` — the error code; defaults to +:invalid+
|
|
1608
|
+
#
|
|
1609
|
+
# _@param_ `message` — human-readable description
|
|
1610
|
+
#
|
|
1611
|
+
# _@param_ `output` — see {#fail_with}
|
|
1612
|
+
def fail_with!: (
|
|
1613
|
+
untyped source,
|
|
1614
|
+
?code: Symbol,
|
|
1615
|
+
?message: String?,
|
|
1616
|
+
?output: ::Hash[Symbol, Object]?
|
|
1617
|
+
) -> void
|
|
1618
|
+
|
|
1619
|
+
# Record a {Success} as the run's result without halting (decision D4).
|
|
1620
|
+
#
|
|
1621
|
+
# _@param_ `message` — human-readable description
|
|
1622
|
+
#
|
|
1623
|
+
# _@param_ `output` — the success output payload
|
|
1624
|
+
#
|
|
1625
|
+
# _@return_ — so a step can branch on the call
|
|
1626
|
+
def succeed: (?String? message, **Object output) -> bool
|
|
1627
|
+
|
|
1628
|
+
# Record a {Failure} and halt the run, skipping the remaining steps
|
|
1629
|
+
# (decision D4).
|
|
1630
|
+
#
|
|
1631
|
+
# _@param_ `code` — the error code
|
|
1632
|
+
#
|
|
1633
|
+
# _@param_ `message` — human-readable description
|
|
1634
|
+
#
|
|
1635
|
+
# _@param_ `errors` — see {#fail}
|
|
1636
|
+
def fail!: (Symbol code, ?String? message, **(String | ::Array[String] | ::Hash[Symbol, Object] | FieldStruct::Base) errors) -> void
|
|
1637
|
+
|
|
1638
|
+
# Record a {Success} and halt the run, skipping the remaining steps
|
|
1639
|
+
# (decision D4).
|
|
1640
|
+
#
|
|
1641
|
+
# _@param_ `message` — human-readable description
|
|
1642
|
+
#
|
|
1643
|
+
# _@param_ `output` — the success output payload
|
|
1644
|
+
def succeed!: (?String? message, **Object output) -> void
|
|
1645
|
+
|
|
1646
|
+
# Record a {Skipped} as the run's result without halting — the action had
|
|
1647
|
+
# nothing to do (decision D17). Not a failure; later steps still run and the
|
|
1648
|
+
# final result reflects the most recent record (last write wins).
|
|
1649
|
+
#
|
|
1650
|
+
# A code declared with {Action.skips} attaches its metadata to the skip,
|
|
1651
|
+
# and its declared message when +message+ is +nil+ (decision D21). Once the
|
|
1652
|
+
# action declares any skip, an undeclared +code+ raises.
|
|
1653
|
+
#
|
|
1654
|
+
# _@param_ `code` — the skip reason; defaults to +:skipped+
|
|
1655
|
+
#
|
|
1656
|
+
# _@param_ `message` — human-readable description
|
|
1657
|
+
#
|
|
1658
|
+
# _@param_ `output` — the skip output payload — captured best-effort like a {Success}'s output, but never validated (decision D17). Lets an idempotent hit return the existing record's data.
|
|
1659
|
+
#
|
|
1660
|
+
# _@return_ — so a step can branch on the call (the run did not succeed here)
|
|
1661
|
+
def skip: (?Symbol code, ?String? message, **Object output) -> bool
|
|
1662
|
+
|
|
1663
|
+
# Record a {Skipped} and halt the run, skipping the remaining steps — the
|
|
1664
|
+
# common "nothing to do, stop here" case (decision D17).
|
|
1665
|
+
#
|
|
1666
|
+
# _@param_ `code` — the skip reason; defaults to +:skipped+
|
|
1667
|
+
#
|
|
1668
|
+
# _@param_ `message` — human-readable description
|
|
1669
|
+
#
|
|
1670
|
+
# _@param_ `output` — the skip output payload (see {#skip})
|
|
1671
|
+
def skip!: (?Symbol code, ?String? message, **Object output) -> void
|
|
1672
|
+
|
|
1673
|
+
# Build a {Failure} for +code+ carrying what {Action.fails} declared for it:
|
|
1674
|
+
# its metadata, and its message when +message+ is +nil+ (decision D20).
|
|
1675
|
+
#
|
|
1676
|
+
# _@param_ `code` — the error code
|
|
1677
|
+
#
|
|
1678
|
+
# _@param_ `message` — the message given at the call
|
|
1679
|
+
def declared_failure: ((Symbol | String) code, String? message) -> Failure
|
|
1680
|
+
|
|
1681
|
+
# Fail this run with a nested action's failure and halt (decision D20).
|
|
1682
|
+
# {Steps::Action} calls it instead of +fail!+, so the child's failure
|
|
1683
|
+
# crosses intact: its code, message, every error (on any field name, since
|
|
1684
|
+
# no keyword list is involved), its metadata, and its result class with any
|
|
1685
|
+
# extra attributes a host subclass adds. Under the child's metadata sits
|
|
1686
|
+
# what this action declares for the code, so the parent's declaration fills
|
|
1687
|
+
# in keys the child didn't set. The child's output and history stay on the
|
|
1688
|
+
# child. A declaring parent must declare the child's code too.
|
|
1689
|
+
#
|
|
1690
|
+
# Under typed outcomes the child's output crosses as this action's failure
|
|
1691
|
+
# output, so it is held to the schema this action declares for the code,
|
|
1692
|
+
# not the child's (decision D23), and the failure becomes this action's own
|
|
1693
|
+
# +Failure+ when {.run} returns.
|
|
1694
|
+
#
|
|
1695
|
+
# _@param_ `child` — the nested action's failure
|
|
1696
|
+
def adopt_failure!: (Failure child) -> void
|
|
1697
|
+
|
|
1698
|
+
# The class a nested failure crosses as: a host's own result subclass
|
|
1699
|
+
# stays (D20), but a typed child's own +Failure+ becomes the gem class,
|
|
1700
|
+
# since it names the child; a typed parent then makes it its own (D23).
|
|
1701
|
+
#
|
|
1702
|
+
# _@param_ `child`
|
|
1703
|
+
def adopted_failure_class: (Result child) -> singleton(Failure)
|
|
1704
|
+
|
|
1705
|
+
# Whether a nested failure's output crosses into this action: under typed
|
|
1706
|
+
# outcomes, for every code but those the gem builds, which keep the output
|
|
1707
|
+
# they had before typed outcomes (none) (decision D23).
|
|
1708
|
+
#
|
|
1709
|
+
# _@param_ `child`
|
|
1710
|
+
def crosses_output?: (Failure child) -> bool
|
|
1711
|
+
|
|
1712
|
+
# _@param_ `output` — a nested failure's output
|
|
1713
|
+
#
|
|
1714
|
+
# _@return_ — its fields, for this action's schema
|
|
1715
|
+
def crossing_output: ((FieldStruct::Base | ::Hash[untyped, untyped] | Object) output) -> ::Hash[Symbol, Object]
|
|
1716
|
+
|
|
1717
|
+
# A +fail+ / +skip+ code must be an identifier-like Symbol or String
|
|
1718
|
+
# ({CODE_FORMAT}): +fail 'card expired'+ is +Kernel#fail+ habit, and in an
|
|
1719
|
+
# action it would record the sentence as a code and keep running (owner
|
|
1720
|
+
# ruling R5).
|
|
1721
|
+
#
|
|
1722
|
+
# _@param_ `kind`
|
|
1723
|
+
#
|
|
1724
|
+
# _@param_ `code`
|
|
1725
|
+
def ensure_code_identifier!: (Symbol kind, Object code) -> void
|
|
1726
|
+
|
|
1727
|
+
# Strictness (decision D20): once an action declares any failure, every
|
|
1728
|
+
# code it fails with must be declared, so its declarations are its whole
|
|
1729
|
+
# failure contract. An action that declares none is unchecked, and the
|
|
1730
|
+
# {GEM_FAILURE_CODES} are always allowed.
|
|
1731
|
+
#
|
|
1732
|
+
# _@param_ `code`
|
|
1733
|
+
def ensure_failure_declared!: (Symbol code) -> void
|
|
1734
|
+
|
|
1735
|
+
# Under typed outcomes, output given for a skip or failure must have a
|
|
1736
|
+
# schema to go into (decision D23): output for a code with none would be
|
|
1737
|
+
# dropped, so it is refused instead. The failures the gem builds keep
|
|
1738
|
+
# their output, so they need none. +output:+ itself must be a Hash or a
|
|
1739
|
+
# struct, which catches an old +output:+ error message.
|
|
1740
|
+
#
|
|
1741
|
+
# _@param_ `verb` — the declaration that would add the schema
|
|
1742
|
+
#
|
|
1743
|
+
# _@param_ `code`
|
|
1744
|
+
#
|
|
1745
|
+
# _@param_ `output` — the output given, if any
|
|
1746
|
+
def ensure_output_declared!: (Symbol verb, Symbol code, (::Hash[untyped, untyped] | FieldStruct::Base)? output) -> void
|
|
1747
|
+
|
|
1748
|
+
# Strictness (decision D21): once an action declares any skip, every code
|
|
1749
|
+
# it skips with must be declared, the implicit default +:skipped+ included.
|
|
1750
|
+
# An action that declares none is unchecked.
|
|
1751
|
+
#
|
|
1752
|
+
# _@param_ `code`
|
|
1753
|
+
def ensure_skip_declared!: (Symbol code) -> void
|
|
1754
|
+
|
|
1755
|
+
# Halt the run immediately, keeping whatever result was already recorded
|
|
1756
|
+
# (a prior +fail+/+succeed+/+skip+, or auto-{Success} if none). Control flow
|
|
1757
|
+
# only — not an exception (decision D4).
|
|
1758
|
+
def halt!: () -> void
|
|
1759
|
+
|
|
1760
|
+
# Wrap this action's run in +model.transaction(**options)+.
|
|
1761
|
+
#
|
|
1762
|
+
# transactional model: :invoice
|
|
1763
|
+
# transactional model: Invoice, requires_new: false # join an outer one
|
|
1764
|
+
#
|
|
1765
|
+
# Run inside a transaction that is already open, the action gets its own
|
|
1766
|
+
# savepoint (+requires_new: true+) unless +requires_new:+ is given, so a
|
|
1767
|
+
# recorded +Failure+ rolls back its own writes rather than leaving them for
|
|
1768
|
+
# the outer transaction to commit.
|
|
1769
|
+
#
|
|
1770
|
+
# _@param_ `model` — the model whose +transaction+ is used; a Symbol is resolved to a constant (+:invoice+ → +Invoice+)
|
|
1771
|
+
#
|
|
1772
|
+
# _@param_ `options` — options forwarded to +transaction+
|
|
1773
|
+
def self.transactional: (model: (Symbol | Class), **Object options) -> void
|
|
1774
|
+
|
|
1775
|
+
# The transaction config for this action, inherited from ancestors when
|
|
1776
|
+
# not set locally (decision D9: inherited, overridable).
|
|
1777
|
+
#
|
|
1778
|
+
# _@return_ — +{model:, options:}+, or +nil+ when
|
|
1779
|
+
# neither this action nor an ancestor is +transactional+
|
|
1780
|
+
def self.transaction_config: () -> ::Hash[Symbol, Object]?
|
|
1781
|
+
|
|
1782
|
+
# Opt this action in (or, with +enabled: false+, back out) to {Job}
|
|
1783
|
+
# honoring the +retry:+ metadata of its declared failures and skips:
|
|
1784
|
+
# +retry: true+ retries the job, +retry: false+ does not. A code with no
|
|
1785
|
+
# +retry:+ key keeps the default mapping. Inherited; a subclass can turn
|
|
1786
|
+
# it off.
|
|
1787
|
+
#
|
|
1788
|
+
# class Reindex < Actionable::Action
|
|
1789
|
+
# honor_retry_metadata
|
|
1790
|
+
# skips :not_ready, retry: true # the job retries until the index is ready
|
|
1791
|
+
# end
|
|
1792
|
+
#
|
|
1793
|
+
# _@param_ `enabled`
|
|
1794
|
+
def self.honor_retry_metadata: (?enabled: bool) -> void
|
|
1795
|
+
|
|
1796
|
+
# Whether {Job} honors this action's +retry:+ metadata, inherited from
|
|
1797
|
+
# ancestors when not set locally. +false+ unless an ancestor opted in.
|
|
1798
|
+
def self.honor_retry_metadata?: () -> bool
|
|
1799
|
+
|
|
1800
|
+
# _@return_ — the declared output schema, or
|
|
1801
|
+
# +nil+ for a free-form action
|
|
1802
|
+
def self.output_schema: () -> singleton(FieldStruct::Base)?
|
|
1803
|
+
|
|
1804
|
+
# _@return_ — the declared input schema, or
|
|
1805
|
+
# +nil+ for a free-form action
|
|
1806
|
+
def self.input_schema: () -> singleton(FieldStruct::Base)?
|
|
1807
|
+
|
|
1808
|
+
# _@return_ — the discriminated-input dispatch, or +nil+
|
|
1809
|
+
# when input isn't discriminated
|
|
1810
|
+
def self.input_dispatch: () -> InputDispatch?
|
|
1811
|
+
|
|
1812
|
+
# _@return_ — the sampling rate for +:sampled+ mode, else +nil+
|
|
1813
|
+
def self.measure_rate: () -> Float?
|
|
1814
|
+
|
|
1815
|
+
# The result recorded during the run, if any. The control-flow methods
|
|
1816
|
+
# (+fail+/+succeed+/+fail!+/+succeed!+) write here; +nil+ means "no explicit
|
|
1817
|
+
# result", which the {Runner} turns into an auto-{Success}.
|
|
1818
|
+
attr_reader result: Result?
|
|
1819
|
+
|
|
1820
|
+
# The typed input struct for this run, when an +input+ schema is declared
|
|
1821
|
+
# (decision D7); +nil+ for a free-form action. Steps read +input.field+.
|
|
1822
|
+
attr_reader input: FieldStruct::Base?
|
|
1823
|
+
end
|
|
1824
|
+
|
|
1825
|
+
# Base class for every error this gem raises. Rescue +Actionable::Error+ to
|
|
1826
|
+
# catch the whole family. Genuine errors are never caught by the runner for
|
|
1827
|
+
# control flow — they propagate to the caller (decision D4).
|
|
1828
|
+
class Error < StandardError
|
|
1829
|
+
end
|
|
1830
|
+
|
|
1831
|
+
# Raised when an action is misconfigured — a reserved output field name, or a
|
|
1832
|
+
# declared step whose method isn't implemented. Surfaced early and clearly so
|
|
1833
|
+
# humans and agents get an actionable message instead of a deep +NoMethodError+
|
|
1834
|
+
# (decision D18).
|
|
1835
|
+
class DefinitionError < Actionable::Error
|
|
1836
|
+
end
|
|
1837
|
+
|
|
1838
|
+
# The single value object a run returns. {Success} and {Failure} are the
|
|
1839
|
+
# concrete subclasses; +Result+ itself is abstract and defines the shared,
|
|
1840
|
+
# FieldStruct-backed shape (decision D5).
|
|
1841
|
+
#
|
|
1842
|
+
# @abstract Instantiate {Success} or {Failure}, not +Result+ directly.
|
|
1843
|
+
class Result < FieldStruct::Base
|
|
1844
|
+
# _@return_ — +:success+ for a {Success}; the error symbol for a {Failure}
|
|
1845
|
+
def code: () -> Symbol
|
|
1846
|
+
|
|
1847
|
+
# _@return_ — human-readable description of the outcome
|
|
1848
|
+
def message: () -> String?
|
|
1849
|
+
|
|
1850
|
+
# _@return_ — the run's declared, typed output payload (empty by default).
|
|
1851
|
+
# Firms up into an output FieldStruct once output schemas land (D6).
|
|
1852
|
+
def output: () -> Object
|
|
1853
|
+
|
|
1854
|
+
# _@return_ — the run's execution history (empty by default).
|
|
1855
|
+
# Firms up into a +History+ once measurement lands (D10).
|
|
1856
|
+
def history: () -> Object
|
|
1857
|
+
|
|
1858
|
+
# _@return_ — whether the run succeeded. Abstract here; each
|
|
1859
|
+
# subclass flips the predicate that applies to it.
|
|
1860
|
+
def success?: () -> bool
|
|
1861
|
+
|
|
1862
|
+
# _@return_ — whether the run failed.
|
|
1863
|
+
def failure?: () -> bool
|
|
1864
|
+
|
|
1865
|
+
# _@return_ — whether the run was skipped — nothing to do, neither a
|
|
1866
|
+
# success nor a failure (decision D17). Abstract here; {Skipped} flips it.
|
|
1867
|
+
def skipped?: () -> bool
|
|
1868
|
+
|
|
1869
|
+
# _@return_ — alias of {#success?}
|
|
1870
|
+
def successful?: () -> bool
|
|
1871
|
+
|
|
1872
|
+
# _@return_ — alias of {#failure?}
|
|
1873
|
+
def failed?: () -> bool
|
|
1874
|
+
|
|
1875
|
+
# _@return_ — whether the run did not fail — true for both a
|
|
1876
|
+
# {Success} and a {Skipped}. The "happy path didn't error" check.
|
|
1877
|
+
def ok?: () -> bool
|
|
1878
|
+
|
|
1879
|
+
# _@return_ — a compact, field-sorted, deterministic representation —
|
|
1880
|
+
# the declared attributes plus the +errors+ collection, sorted by name,
|
|
1881
|
+
# so equal results always render identically.
|
|
1882
|
+
def inspect: () -> String
|
|
1883
|
+
|
|
1884
|
+
# _@return_ — same deterministic representation as {#inspect}
|
|
1885
|
+
def to_s: () -> String
|
|
1886
|
+
|
|
1887
|
+
# _@return_ — the outcome as a single symbol — +:success+, +:failure+,
|
|
1888
|
+
# or +:skipped+. The coarse-grained companion to the specific {#code}.
|
|
1889
|
+
def status: () -> Symbol
|
|
1890
|
+
|
|
1891
|
+
# A plain-Hash view of the result — +code+, +message+, the +output+ and
|
|
1892
|
+
# +history+ rendered to primitives, and the +errors+ collection as a Hash
|
|
1893
|
+
# (decision D19). The serializable companion to {#inspect}.
|
|
1894
|
+
def to_h: () -> ::Hash[Symbol, Object]
|
|
1895
|
+
|
|
1896
|
+
# A compact element for an HTTP/batch response: the item's position, its
|
|
1897
|
+
# {#status}, the id read off the typed output, and its errors (decision
|
|
1898
|
+
# D19). Lets a controller map a batch of results without hand-rolling the
|
|
1899
|
+
# shape.
|
|
1900
|
+
#
|
|
1901
|
+
# results.each_with_index.map { |r, i| r.to_api_h(index: i) }
|
|
1902
|
+
#
|
|
1903
|
+
# _@param_ `index` — the element's position in a batch
|
|
1904
|
+
#
|
|
1905
|
+
# _@param_ `id_field` — the output field to surface as +:id+ (default +:id+)
|
|
1906
|
+
def to_api_h: (?index: Integer?, ?id_field: Symbol) -> ::Hash[Symbol, Object]
|
|
1907
|
+
|
|
1908
|
+
# Convenience delegation: a declared output field is readable straight off
|
|
1909
|
+
# the result (+result.invoice+ → +result.output.invoice+). Only fields
|
|
1910
|
+
# declared on a typed output schema delegate; incidental data never does
|
|
1911
|
+
# (decision D6).
|
|
1912
|
+
#
|
|
1913
|
+
# _@param_ `name` — the method name asked about (Ruby passes a Symbol)
|
|
1914
|
+
#
|
|
1915
|
+
# _@param_ `include_private` — whether private methods count
|
|
1916
|
+
def respond_to_missing?: (Symbol name, ?bool include_private) -> bool
|
|
1917
|
+
|
|
1918
|
+
# _@param_ `name` — the method called
|
|
1919
|
+
#
|
|
1920
|
+
# _@param_ `args` — its arguments, forwarded to the output
|
|
1921
|
+
#
|
|
1922
|
+
# _@return_ — the output field's value: left untyped on purpose,
|
|
1923
|
+
# like any field reader whose type the schema decides
|
|
1924
|
+
#
|
|
1925
|
+
# _@see_ `#respond_to_missing?`
|
|
1926
|
+
def method_missing: (Symbol name, *Object args) -> untyped
|
|
1927
|
+
|
|
1928
|
+
# _@param_ `name`
|
|
1929
|
+
#
|
|
1930
|
+
# _@return_ — whether +name+ is a declared field on a typed output
|
|
1931
|
+
def delegates_to_output?: (Symbol name) -> bool
|
|
1932
|
+
|
|
1933
|
+
# Read +name+ off the output — a declared field on a typed output, a key on
|
|
1934
|
+
# a Hash output, or +nil+ when absent.
|
|
1935
|
+
#
|
|
1936
|
+
# _@param_ `name`
|
|
1937
|
+
def output_field: (Symbol name) -> Object?
|
|
1938
|
+
|
|
1939
|
+
# The output fields the run actually set (captured instance variables and
|
|
1940
|
+
# +succeed+/+skip+ kwargs), as opposed to fields left nil or at their
|
|
1941
|
+
# default. A parent absorbing a nested action's output copies only these
|
|
1942
|
+
# (decision D3). +nil+ when unknown, e.g. a hand-built result.
|
|
1943
|
+
attr_accessor output_fields_set: ::Array[Symbol]?
|
|
1944
|
+
end
|
|
1945
|
+
|
|
1946
|
+
# Executes an action instance's steps and produces the {Result}.
|
|
1947
|
+
#
|
|
1948
|
+
# The main loop runs inside {Actionable.catch_halt}, so a halting step
|
|
1949
|
+
# (+throw Actionable::HALT+) unwinds out of the loop without running the
|
|
1950
|
+
# remaining steps. Genuine exceptions are not caught — they propagate to the
|
|
1951
|
+
# caller (decision D4). When no step recorded a result, the run auto-succeeds.
|
|
1952
|
+
#
|
|
1953
|
+
# After the main steps the runner picks the lifecycle hook category from the
|
|
1954
|
+
# outcome (+on_success+ vs +on_failure+) and runs it, then the +always+ hooks
|
|
1955
|
+
# (decision D2). Hooks are ordinary methods: they may record via the
|
|
1956
|
+
# control-flow verbs, and a halting verb (+fail!+ / +skip!+ / +succeed!+ /
|
|
1957
|
+
# +halt!+) has its result adopted, then stops the remaining hooks of its own
|
|
1958
|
+
# phase: a halt in an outcome hook skips the rest of that category but the
|
|
1959
|
+
# +always+ hooks still run (seeing the adopted result), and a halt in an
|
|
1960
|
+
# +always+ hook skips the remaining +always+ hooks. The output is refreshed
|
|
1961
|
+
# (re-captured and re-validated) after the main steps and after each hook
|
|
1962
|
+
# (decision D6).
|
|
1963
|
+
#
|
|
1964
|
+
# When the action (or an enclosing measuring run) enables measurement, the
|
|
1965
|
+
# runner records a {History} of every step — section, name, timing, result
|
|
1966
|
+
# code, and nested child history (decision D10).
|
|
1967
|
+
class Runner
|
|
1968
|
+
include Actionable::Rails::RunnerAfterCommit
|
|
1969
|
+
include Actionable::Rails::RunnerTransaction
|
|
1970
|
+
|
|
1971
|
+
# _@param_ `instance` — the action instance to run
|
|
1972
|
+
def initialize: (Action _instance) -> void
|
|
1973
|
+
|
|
1974
|
+
# Run the action's steps and lifecycle hooks, returning the finalized result.
|
|
1975
|
+
#
|
|
1976
|
+
# _@return_ — the recorded result (or a fresh {Success} if none was
|
|
1977
|
+
# set), with its typed output and (when measuring) history assigned
|
|
1978
|
+
def run: () -> Result
|
|
1979
|
+
|
|
1980
|
+
# Run the body, marking measurement active so nested runs cascade.
|
|
1981
|
+
#
|
|
1982
|
+
# _@return_ — the block's value
|
|
1983
|
+
def measured: () ?{ () -> Object } -> Object
|
|
1984
|
+
|
|
1985
|
+
# The main steps, then the outcome and always lifecycle hooks.
|
|
1986
|
+
def perform: () -> void
|
|
1987
|
+
|
|
1988
|
+
# Run each declared step in order, skipping any whose guard opts out.
|
|
1989
|
+
def run_steps: () -> void
|
|
1990
|
+
|
|
1991
|
+
# Run the outcome-appropriate hooks then the +always+ hooks, each phase in
|
|
1992
|
+
# its own halt catch: a hook's halt stops the remaining hooks of its phase
|
|
1993
|
+
# only, so +always+ hooks run even after an outcome hook halts. The
|
|
1994
|
+
# category is fixed by the post-main-steps outcome — it does not re-dispatch
|
|
1995
|
+
# if a hook later flips the result.
|
|
1996
|
+
def run_lifecycle: () -> void
|
|
1997
|
+
|
|
1998
|
+
# Run one hook, then adopt any result it recorded and refresh the output.
|
|
1999
|
+
# A hook that halts is caught here first so its result is adopted too, then
|
|
2000
|
+
# the halt is re-thrown to stop the remaining hooks of the phase.
|
|
2001
|
+
#
|
|
2002
|
+
# _@param_ `section`
|
|
2003
|
+
#
|
|
2004
|
+
# _@param_ `hook`
|
|
2005
|
+
def run_hook: (Symbol section, Steps::Base hook) -> void
|
|
2006
|
+
|
|
2007
|
+
# Run one main step, skipping it when its guard opts out.
|
|
2008
|
+
#
|
|
2009
|
+
# _@param_ `section`
|
|
2010
|
+
#
|
|
2011
|
+
# _@param_ `step`
|
|
2012
|
+
def call_step: (Symbol section, Steps::Base step) -> void
|
|
2013
|
+
|
|
2014
|
+
# Time and record a step into the history when measuring; otherwise just run
|
|
2015
|
+
# it. The result code is recorded only when the step changed the recorded
|
|
2016
|
+
# result, so steps that merely set ivars carry a nil code.
|
|
2017
|
+
#
|
|
2018
|
+
# _@param_ `section`
|
|
2019
|
+
#
|
|
2020
|
+
# _@param_ `step`
|
|
2021
|
+
def record: (Symbol section, Steps::Base step) -> void
|
|
2022
|
+
|
|
2023
|
+
# Capture, coerce, and attach the typed output (decision D6). Free-form
|
|
2024
|
+
# actions (no schema) keep whatever output was recorded. With a schema,
|
|
2025
|
+
# capture the declared-field ivars overlaid with any +succeed+ kwargs
|
|
2026
|
+
# (kwargs win) and coerce them. Only a strict {Success} is validated — its
|
|
2027
|
+
# output failing validation means it can't really have succeeded, so
|
|
2028
|
+
# +@result+ becomes a +Failure(:invalid_output)+.
|
|
2029
|
+
#
|
|
2030
|
+
# A {Failure} or {Skipped} whose code declares its own output schema
|
|
2031
|
+
# (decision D23) is built from its kwargs alone and validated the same way.
|
|
2032
|
+
# Without one it carries an empty output under typed outcomes, and
|
|
2033
|
+
# otherwise a best-effort, unvalidated capture into the success schema
|
|
2034
|
+
# (decisions D6/D17). The failures the gem builds keep their output.
|
|
2035
|
+
def refresh_output: () -> void
|
|
2036
|
+
|
|
2037
|
+
def refresh_success_output: () -> void
|
|
2038
|
+
|
|
2039
|
+
# Build the output struct and note on the result which fields the run
|
|
2040
|
+
# actually set, so a parent absorbing this run copies only those (D3).
|
|
2041
|
+
#
|
|
2042
|
+
# _@param_ `schema`
|
|
2043
|
+
#
|
|
2044
|
+
# _@param_ `attrs`
|
|
2045
|
+
def build_output: (singleton(FieldStruct::Base) schema, ::Hash[Symbol, Object] attrs) -> FieldStruct::Base
|
|
2046
|
+
|
|
2047
|
+
# The result the action instance recorded, refusing anything else stored
|
|
2048
|
+
# in the reserved +@result+ ivar with a clear error instead of a deep
|
|
2049
|
+
# +NoMethodError+.
|
|
2050
|
+
def instance_result: () -> Result?
|
|
2051
|
+
|
|
2052
|
+
# Attach +output+ when it is valid; otherwise the run ends as a
|
|
2053
|
+
# +Failure(:invalid_output)+ carrying it.
|
|
2054
|
+
#
|
|
2055
|
+
# _@param_ `output`
|
|
2056
|
+
def attach_validated: (FieldStruct::Base output) -> void
|
|
2057
|
+
|
|
2058
|
+
# Attach a best-effort output to a failure or skip without validating it
|
|
2059
|
+
# (decisions D6/D17). Assigning a struct makes the result validate it, which
|
|
2060
|
+
# would add an +output+ error, so only the errors the result already had
|
|
2061
|
+
# are kept.
|
|
2062
|
+
#
|
|
2063
|
+
# _@param_ `output`
|
|
2064
|
+
def attach_unvalidated: (FieldStruct::Base output) -> void
|
|
2065
|
+
|
|
2066
|
+
# _@return_ — the output schema the skip or
|
|
2067
|
+
# failure's code declares (decision D23), if any
|
|
2068
|
+
def outcome_schema: () -> singleton(FieldStruct::Base)?
|
|
2069
|
+
|
|
2070
|
+
# A skip or failure's output fields, from the kwargs it was recorded with,
|
|
2071
|
+
# never from instance variables (decision D23). After a hook refresh the
|
|
2072
|
+
# output is already the struct, so its own attributes are reused, limited
|
|
2073
|
+
# to the fields the kwargs set so the set-fields marker stays true.
|
|
2074
|
+
#
|
|
2075
|
+
# _@param_ `schema`
|
|
2076
|
+
def outcome_attributes: (singleton(FieldStruct::Base) schema) -> ::Hash[Symbol, Object]
|
|
2077
|
+
|
|
2078
|
+
# _@return_ — whether the result is a failure the gem itself built
|
|
2079
|
+
# ({Action::GEM_FAILURE_CODES}), whose output is left as it is
|
|
2080
|
+
def gem_failure?: () -> bool
|
|
2081
|
+
|
|
2082
|
+
# Declared-field ivars from the action instance, overlaid with the recorded
|
|
2083
|
+
# +succeed+ / +skip+ kwargs, kwargs winning on conflict.
|
|
2084
|
+
#
|
|
2085
|
+
# _@param_ `schema`
|
|
2086
|
+
def captured_attributes: (singleton(FieldStruct::Base) schema) -> ::Hash[Symbol, Object]
|
|
2087
|
+
|
|
2088
|
+
# The +succeed+ / +skip+ kwargs recorded with the current result. They sit
|
|
2089
|
+
# on +@result.output+ as a Hash only until the first refresh replaces it
|
|
2090
|
+
# with the struct, so they are kept here to overlay every later refresh
|
|
2091
|
+
# (after each hook) as well.
|
|
2092
|
+
def recorded_overrides: () -> ::Hash[Symbol, Object]
|
|
2093
|
+
|
|
2094
|
+
# _@param_ `output` — the invalid output struct
|
|
2095
|
+
#
|
|
2096
|
+
# _@return_ — code +:invalid_output+, with the output's validation
|
|
2097
|
+
# errors copied onto the failure and the (invalid) output attached
|
|
2098
|
+
def invalid_output_failure: (FieldStruct::Base output) -> Failure
|
|
2099
|
+
|
|
2100
|
+
# _@return_ — the running action's transaction config
|
|
2101
|
+
def transaction_config: () -> ::Hash[Symbol, Object]?
|
|
2102
|
+
|
|
2103
|
+
# The configured options, plus +requires_new: true+ when a transaction is
|
|
2104
|
+
# already open and the action did not say otherwise. Joining the outer
|
|
2105
|
+
# transaction would swallow this run's rollback, so a recorded failure's
|
|
2106
|
+
# writes would commit with it (decision D9).
|
|
2107
|
+
#
|
|
2108
|
+
# _@param_ `model`
|
|
2109
|
+
#
|
|
2110
|
+
# _@param_ `options`
|
|
2111
|
+
def transaction_options: (Class model, ::Hash[Symbol, Object] options) -> ::Hash[Symbol, Object]
|
|
2112
|
+
|
|
2113
|
+
# _@param_ `model`
|
|
2114
|
+
#
|
|
2115
|
+
# _@return_ — the resolved model class
|
|
2116
|
+
def resolve_model: ((Symbol | Class) model) -> Class
|
|
2117
|
+
|
|
2118
|
+
def schedule_after_commit: () -> void
|
|
2119
|
+
|
|
2120
|
+
# Run the hooks in order. A halt stops the remaining ones; nothing they
|
|
2121
|
+
# record reaches the already-returned result.
|
|
2122
|
+
#
|
|
2123
|
+
# _@param_ `hooks`
|
|
2124
|
+
def run_after_commit_hooks: (::Array[Steps::Base] hooks) -> void
|
|
2125
|
+
end
|
|
2126
|
+
|
|
2127
|
+
# An ordered record of the steps a run executed, when measurement is enabled
|
|
2128
|
+
# (decision D10). Each entry is a {History::Step}; nested action steps carry
|
|
2129
|
+
# their child's +History+.
|
|
2130
|
+
class History
|
|
2131
|
+
def initialize: () -> void
|
|
2132
|
+
|
|
2133
|
+
# Append a recorded step.
|
|
2134
|
+
#
|
|
2135
|
+
# _@param_ `step`
|
|
2136
|
+
def <<: (Step step) -> self
|
|
2137
|
+
|
|
2138
|
+
# _@return_ — whether any step has been recorded
|
|
2139
|
+
def empty?: () -> bool
|
|
2140
|
+
|
|
2141
|
+
# _@return_ — the summed duration of every recorded step
|
|
2142
|
+
def took: () -> Float
|
|
2143
|
+
|
|
2144
|
+
# _@return_ — each step as a JSON-ready hash
|
|
2145
|
+
def as_json: () -> ::Array[::Hash[untyped, untyped]]
|
|
2146
|
+
|
|
2147
|
+
# _@return_ — the history serialized to JSON via Oj (decision D15).
|
|
2148
|
+
# +:compat+ mode emits plain JSON (string keys, symbols as strings) rather
|
|
2149
|
+
# than Oj's default object-marker encoding.
|
|
2150
|
+
def to_json: () -> String
|
|
2151
|
+
|
|
2152
|
+
# _@return_ — the recorded steps, in execution order
|
|
2153
|
+
attr_reader steps: ::Array[Step]
|
|
2154
|
+
|
|
2155
|
+
# One recorded step: which section it ran in, its name, when it started, how
|
|
2156
|
+
# long it took, the result code it produced (if any), and — for action
|
|
2157
|
+
# steps — the nested child {History}.
|
|
2158
|
+
class Step
|
|
2159
|
+
# _@param_ `section`
|
|
2160
|
+
#
|
|
2161
|
+
# _@param_ `name`
|
|
2162
|
+
#
|
|
2163
|
+
# _@param_ `started_at`
|
|
2164
|
+
def initialize: (section: Symbol, name: (Symbol | String | Class), started_at: Time) -> void
|
|
2165
|
+
|
|
2166
|
+
# _@return_ — a JSON-ready hash (nested history recurses)
|
|
2167
|
+
def as_json: () -> ::Hash[Symbol, Object]
|
|
2168
|
+
|
|
2169
|
+
# _@return_ — +:main+, +:success+, +:failure+, or +:always+
|
|
2170
|
+
attr_reader section: Symbol
|
|
2171
|
+
|
|
2172
|
+
# _@return_ — the step's name
|
|
2173
|
+
attr_reader name: (Symbol | String | Class)
|
|
2174
|
+
|
|
2175
|
+
# _@return_ — wall-clock start time
|
|
2176
|
+
attr_reader started_at: Time
|
|
2177
|
+
|
|
2178
|
+
# _@return_ — elapsed seconds (monotonic)
|
|
2179
|
+
attr_accessor duration: Float
|
|
2180
|
+
|
|
2181
|
+
# _@return_ — the result code this step recorded, if it set one
|
|
2182
|
+
attr_accessor code: Symbol?
|
|
2183
|
+
|
|
2184
|
+
# _@return_ — the nested action's history, for action steps
|
|
2185
|
+
attr_accessor nested: History?
|
|
2186
|
+
end
|
|
2187
|
+
end
|
|
2188
|
+
|
|
2189
|
+
# A read-only map of every named action class, keyed by class name (decision
|
|
2190
|
+
# D11). {Action} registers each subclass through its +inherited+ hook;
|
|
2191
|
+
# anonymous subclasses (which have no name) are skipped. Used for discovery
|
|
2192
|
+
# and tooling — e.g. sweeping all actions to generate RBS.
|
|
2193
|
+
class Registry
|
|
2194
|
+
def initialize: () -> void
|
|
2195
|
+
|
|
2196
|
+
# Register an action class under its name. No-op for anonymous classes.
|
|
2197
|
+
#
|
|
2198
|
+
# _@param_ `klass` — the action subclass
|
|
2199
|
+
def register: (Class klass) -> self
|
|
2200
|
+
|
|
2201
|
+
# _@param_ `name` — an action class name
|
|
2202
|
+
#
|
|
2203
|
+
# _@return_ — the registered class, or +nil+
|
|
2204
|
+
def []: ((String | Symbol) name) -> Class?
|
|
2205
|
+
|
|
2206
|
+
def each: () ?{ (String name, Class klass) -> void } -> (::Enumerator[untyped] | self)
|
|
2207
|
+
|
|
2208
|
+
# _@return_ — the registered class names
|
|
2209
|
+
def keys: () -> ::Array[String]
|
|
2210
|
+
|
|
2211
|
+
# _@return_ — the registered classes
|
|
2212
|
+
def values: () -> ::Array[Class]
|
|
2213
|
+
|
|
2214
|
+
# _@return_ — the number of registered classes
|
|
2215
|
+
def size: () -> Integer
|
|
2216
|
+
|
|
2217
|
+
# _@return_ — whether nothing is registered
|
|
2218
|
+
def empty?: () -> bool
|
|
2219
|
+
end
|
|
2220
|
+
|
|
2221
|
+
# Thread-local measurement context (decision D10). It lets a measuring run
|
|
2222
|
+
# cascade into the nested actions it invokes, and lets an action step attach
|
|
2223
|
+
# its child's history to the step record currently open in the parent runner.
|
|
2224
|
+
#
|
|
2225
|
+
# All state is per-thread and saved/restored around each scope, so concurrent
|
|
2226
|
+
# runs on different threads never interfere.
|
|
2227
|
+
module Measurement
|
|
2228
|
+
# _@return_ — whether the current thread is inside a measuring run
|
|
2229
|
+
def self.active?: () -> bool
|
|
2230
|
+
|
|
2231
|
+
# _@return_ — the step record nested history attaches to
|
|
2232
|
+
def self.open_step: () -> History::Step?
|
|
2233
|
+
|
|
2234
|
+
# Mark measurement active for the block, so nested runs measure too.
|
|
2235
|
+
#
|
|
2236
|
+
# _@return_ — the block's value
|
|
2237
|
+
def self.measuring: () ?{ () -> Object } -> Object
|
|
2238
|
+
|
|
2239
|
+
# Make +record+ the open step for the block, so a nested action step can
|
|
2240
|
+
# attach its child's history to it.
|
|
2241
|
+
#
|
|
2242
|
+
# _@param_ `record`
|
|
2243
|
+
#
|
|
2244
|
+
# _@return_ — the block's value
|
|
2245
|
+
def self.recording: (History::Step record) ?{ () -> Object } -> Object
|
|
2246
|
+
|
|
2247
|
+
# This thread's measurement state: +:active+ and +:open_step+.
|
|
2248
|
+
def self.store: () -> ::Hash[Symbol, Object]
|
|
2249
|
+
|
|
2250
|
+
# Merge +changes+ into the state for the block, restoring it afterwards.
|
|
2251
|
+
#
|
|
2252
|
+
# _@param_ `changes` — the keys to override
|
|
2253
|
+
#
|
|
2254
|
+
# _@return_ — the block's value
|
|
2255
|
+
def self.with: (::Hash[Symbol, Object] changes) ?{ () -> Object } -> Object
|
|
2256
|
+
end
|
|
2257
|
+
|
|
2258
|
+
# Shared branch-value matching for the two places Actionable dispatches on a
|
|
2259
|
+
# value: case steps (decision D3) and discriminated input (decision D19). A
|
|
2260
|
+
# branch value matches by Array membership, by +Regexp#match?+ against the
|
|
2261
|
+
# value's string form, or by +==+.
|
|
2262
|
+
module ValueMatch
|
|
2263
|
+
# _@param_ `branch_value` — the declared branch matcher
|
|
2264
|
+
#
|
|
2265
|
+
# _@param_ `value` — the runtime value being dispatched on
|
|
2266
|
+
#
|
|
2267
|
+
# _@return_ — whether +value+ matches +branch_value+
|
|
2268
|
+
def matches?: ((Object | ::Array[untyped] | Regexp) branch_value, Object value) -> bool
|
|
2269
|
+
|
|
2270
|
+
# _@param_ `branch_value` — the declared branch matcher
|
|
2271
|
+
#
|
|
2272
|
+
# _@param_ `value` — the runtime value being dispatched on
|
|
2273
|
+
#
|
|
2274
|
+
# _@return_ — whether +value+ matches +branch_value+
|
|
2275
|
+
def self.matches?: ((Object | ::Array[untyped] | Regexp) branch_value, Object value) -> bool
|
|
2276
|
+
end
|
|
2277
|
+
|
|
2278
|
+
# The value object returned by {Action.run_each}: an ordered, +Enumerable+
|
|
2279
|
+
# collection of the per-item {Result}s, plus aggregate predicates over them
|
|
2280
|
+
# (decision D19). Each item ran independently — one {Result} per element, in
|
|
2281
|
+
# input order — so a failure in one never affects another.
|
|
2282
|
+
#
|
|
2283
|
+
# batch = ImportRow.run_each(rows) { |row| {row: row} }
|
|
2284
|
+
# batch.all_ok? # => false
|
|
2285
|
+
# batch.failures.size # => 2
|
|
2286
|
+
# batch.to_api_h # => [{index: 0, status: :success, ...}, ...]
|
|
2287
|
+
class BatchResult
|
|
2288
|
+
include ::Enumerable[Result]
|
|
2289
|
+
|
|
2290
|
+
# _@param_ `results`
|
|
2291
|
+
def initialize: (::Array[Result] results) -> void
|
|
2292
|
+
|
|
2293
|
+
def each: () ?{ (Result result) -> void } -> (::Enumerator[untyped] | self)
|
|
2294
|
+
|
|
2295
|
+
# _@param_ `index`
|
|
2296
|
+
#
|
|
2297
|
+
# _@return_ — the result at +index+
|
|
2298
|
+
def []: (Integer index) -> Result?
|
|
2299
|
+
|
|
2300
|
+
# _@return_ — the number of results
|
|
2301
|
+
def size: () -> Integer
|
|
2302
|
+
|
|
2303
|
+
# _@return_ — whether the batch ran no items
|
|
2304
|
+
def empty?: () -> bool
|
|
2305
|
+
|
|
2306
|
+
# _@return_ — whether every result is +ok?+ (success or skip);
|
|
2307
|
+
# vacuously true for an empty batch
|
|
2308
|
+
def all_ok?: () -> bool
|
|
2309
|
+
|
|
2310
|
+
# _@return_ — whether any result is a failure
|
|
2311
|
+
def any_failure?: () -> bool
|
|
2312
|
+
|
|
2313
|
+
# _@return_ — whether the batch is mixed — at least one failure and at
|
|
2314
|
+
# least one +ok?+ result
|
|
2315
|
+
def partial?: () -> bool
|
|
2316
|
+
|
|
2317
|
+
# _@return_ — the successful results
|
|
2318
|
+
def successes: () -> ::Array[Result]
|
|
2319
|
+
|
|
2320
|
+
# _@return_ — the failed results
|
|
2321
|
+
def failures: () -> ::Array[Result]
|
|
2322
|
+
|
|
2323
|
+
# _@return_ — the skipped results
|
|
2324
|
+
def skips: () -> ::Array[Result]
|
|
2325
|
+
|
|
2326
|
+
# The batch as an array of API elements, each tagged with its position
|
|
2327
|
+
# (see {Result#to_api_h}). Ready to render as a JSON array of per-item
|
|
2328
|
+
# statuses.
|
|
2329
|
+
#
|
|
2330
|
+
# _@param_ `id_field` — the output field to surface as +:id+
|
|
2331
|
+
def to_api_h: (?id_field: Symbol) -> ::Array[::Hash[Symbol, Object]]
|
|
2332
|
+
|
|
2333
|
+
# _@return_ — the per-item results, in input order
|
|
2334
|
+
attr_reader results: ::Array[Result]
|
|
2335
|
+
end
|
|
2336
|
+
|
|
2337
|
+
# Runtime selection of an input schema from a discriminator value (decision
|
|
2338
|
+
# D19, declared with +input_for+). Each branch maps a match value to a
|
|
2339
|
+
# FieldStruct schema; an optional +default+ catches the rest. Lets a
|
|
2340
|
+
# polymorphic payload (e.g. +event_type+ → shape) use typed, validated input.
|
|
2341
|
+
#
|
|
2342
|
+
# input_for :event_type do
|
|
2343
|
+
# on 'sale', SaleShape
|
|
2344
|
+
# on 'refund', RefundShape
|
|
2345
|
+
# default UnknownShape
|
|
2346
|
+
# end
|
|
2347
|
+
class InputDispatch
|
|
2348
|
+
# Build a dispatch from an +input_for+ declaration.
|
|
2349
|
+
#
|
|
2350
|
+
# _@param_ `discriminator` — the kwarg whose value selects the schema
|
|
2351
|
+
def self.define: ((Symbol | String) discriminator) -> InputDispatch
|
|
2352
|
+
|
|
2353
|
+
# _@param_ `discriminator`
|
|
2354
|
+
#
|
|
2355
|
+
# _@param_ `branches`
|
|
2356
|
+
#
|
|
2357
|
+
# _@param_ `default_schema`
|
|
2358
|
+
def initialize: (Symbol discriminator, ::Array[[Object, singleton(FieldStruct::Base)]] branches, singleton(FieldStruct::Base)? default_schema) -> void
|
|
2359
|
+
|
|
2360
|
+
# The schema for a discriminator value: the first matching branch, else the
|
|
2361
|
+
# default, else +nil+ (no match and no default).
|
|
2362
|
+
#
|
|
2363
|
+
# _@param_ `value` — the discriminator value
|
|
2364
|
+
def schema_for: (Object value) -> singleton(FieldStruct::Base)?
|
|
2365
|
+
|
|
2366
|
+
# _@return_ — every distinct schema reachable
|
|
2367
|
+
# through this dispatch (branches plus default)
|
|
2368
|
+
def schemas: () -> ::Array[singleton(FieldStruct::Base)]
|
|
2369
|
+
|
|
2370
|
+
# _@return_ — the branch match values, in declaration order
|
|
2371
|
+
def variants: () -> ::Array[Object]
|
|
2372
|
+
|
|
2373
|
+
# _@return_ — the discriminator field read from +.run+'s kwargs
|
|
2374
|
+
attr_reader discriminator: Symbol
|
|
2375
|
+
|
|
2376
|
+
# _@return_ — ordered [match, schema] pairs
|
|
2377
|
+
attr_reader branches: ::Array[[Object, singleton(FieldStruct::Base)]]
|
|
2378
|
+
|
|
2379
|
+
# _@return_ — the fallback schema
|
|
2380
|
+
attr_reader default_schema: singleton(FieldStruct::Base)?
|
|
2381
|
+
|
|
2382
|
+
# Collects +on+/+default+ branch declarations inside an +input_for+ block.
|
|
2383
|
+
class Builder
|
|
2384
|
+
def initialize: () -> void
|
|
2385
|
+
|
|
2386
|
+
# Register a branch. +value+ matches the discriminator by +==+, by
|
|
2387
|
+
# Array membership, or by +Regexp#match?+ (see {ValueMatch}).
|
|
2388
|
+
#
|
|
2389
|
+
# _@param_ `value`
|
|
2390
|
+
#
|
|
2391
|
+
# _@param_ `schema`
|
|
2392
|
+
def on: ((Object | ::Array[untyped] | Regexp) value, singleton(FieldStruct::Base) schema) -> void
|
|
2393
|
+
|
|
2394
|
+
# Register the fallback schema, used when no +on+ branch matches.
|
|
2395
|
+
#
|
|
2396
|
+
# _@param_ `schema`
|
|
2397
|
+
def default: (singleton(FieldStruct::Base) schema) -> void
|
|
2398
|
+
|
|
2399
|
+
# _@return_ — ordered [match, schema] pairs
|
|
2400
|
+
attr_reader branches: ::Array[[Object, singleton(FieldStruct::Base)]]
|
|
2401
|
+
|
|
2402
|
+
# _@return_ — the fallback schema, if declared
|
|
2403
|
+
attr_reader default_schema: singleton(FieldStruct::Base)?
|
|
2404
|
+
end
|
|
2405
|
+
end
|
|
2406
|
+
|
|
2407
|
+
# A failed run's result. Its +code+ is the error symbol describing why the
|
|
2408
|
+
# run failed (e.g. +:not_found+, +:invalid_input+), defaulting to +:error+.
|
|
2409
|
+
class Failure < Actionable::Result
|
|
2410
|
+
# _@return_ — the error code; defaults to +:error+
|
|
2411
|
+
def code: () -> Symbol
|
|
2412
|
+
|
|
2413
|
+
# _@return_ — the metadata the action declared for this
|
|
2414
|
+
# code with {Action.fails} (decision D20), so an adapter can read it off the
|
|
2415
|
+
# result without the action class. Empty when the code isn't declared.
|
|
2416
|
+
# The gem stores and passes it through but never interprets it.
|
|
2417
|
+
def metadata: () -> ::Hash[Symbol, Object]
|
|
2418
|
+
|
|
2419
|
+
def failure?: () -> bool
|
|
2420
|
+
|
|
2421
|
+
# A plain-Hash view of the failure: {Result#to_h} plus its declared
|
|
2422
|
+
# +metadata+ (decision D20).
|
|
2423
|
+
def to_h: () -> ::Hash[Symbol, Object]
|
|
2424
|
+
|
|
2425
|
+
# Copy another object's FieldStruct-style errors onto this failure's own
|
|
2426
|
+
# errors collection. The shared mechanism behind +Failure(:invalid_input)+,
|
|
2427
|
+
# +Failure(:invalid_output)+, and the {Action#fail_with} verb (decision D19).
|
|
2428
|
+
#
|
|
2429
|
+
# _@param_ `source` — any object whose +errors+ responds to +to_h+ (a +FieldStruct::Base+ instance, or another result)
|
|
2430
|
+
#
|
|
2431
|
+
# _@return_ — so callers can build-and-absorb in one expression
|
|
2432
|
+
def absorb_errors_from: (untyped source) -> self
|
|
2433
|
+
end
|
|
2434
|
+
|
|
2435
|
+
# A skipped run's result (decision D17): the action had nothing to do — not a
|
|
2436
|
+
# success (no real work), but not a failure either. Its +code+ is the skip
|
|
2437
|
+
# reason (e.g. +:not_ready+), defaulting to +:skipped+.
|
|
2438
|
+
class Skipped < Actionable::Result
|
|
2439
|
+
# _@return_ — the skip reason; defaults to +:skipped+
|
|
2440
|
+
def code: () -> Symbol
|
|
2441
|
+
|
|
2442
|
+
# _@return_ — the metadata the action declared for this
|
|
2443
|
+
# code with {Action.skips} (decision D21), so an adapter can read it off the
|
|
2444
|
+
# result without the action class. Empty when the code isn't declared.
|
|
2445
|
+
# The gem stores and passes it through but never interprets it.
|
|
2446
|
+
def metadata: () -> ::Hash[Symbol, Object]
|
|
2447
|
+
|
|
2448
|
+
def skipped?: () -> bool
|
|
2449
|
+
|
|
2450
|
+
# A plain-Hash view of the skip: {Result#to_h} plus its declared +metadata+
|
|
2451
|
+
# (decision D21).
|
|
2452
|
+
def to_h: () -> ::Hash[Symbol, Object]
|
|
2453
|
+
end
|
|
2454
|
+
|
|
2455
|
+
# A successful run's result. Its +code+ defaults to +:success+.
|
|
2456
|
+
class Success < Actionable::Result
|
|
2457
|
+
# _@return_ — always +:success+ unless explicitly overridden
|
|
2458
|
+
def code: () -> Symbol
|
|
2459
|
+
|
|
2460
|
+
def success?: () -> bool
|
|
2461
|
+
end
|
|
2462
|
+
|
|
2463
|
+
module Rails
|
|
2464
|
+
# The +after_commit+ hook kind. Prepended onto {Actionable::Action}'s
|
|
2465
|
+
# singleton class by +actionable/rails+, so the core never knows about it
|
|
2466
|
+
# (decision D8).
|
|
2467
|
+
#
|
|
2468
|
+
# An +after_commit+ hook runs once, after the database work of a
|
|
2469
|
+
# successful run has really committed: it is registered with
|
|
2470
|
+
# +ActiveRecord.after_all_transactions_commit+ when the run ends, so it
|
|
2471
|
+
# waits for the outermost open transaction (not the action's own
|
|
2472
|
+
# savepoint), runs immediately when none is open, and never runs if any
|
|
2473
|
+
# enclosing transaction rolls back.
|
|
2474
|
+
module AfterCommit
|
|
2475
|
+
MINIMUM_ACTIVE_RECORD: String
|
|
2476
|
+
|
|
2477
|
+
# Declare a hook that runs after the outermost database transaction
|
|
2478
|
+
# commits, iff the run's final result is a {Success}.
|
|
2479
|
+
#
|
|
2480
|
+
# after_commit :send_receipt
|
|
2481
|
+
# after_commit NotifyWarehouse, input: %i[order]
|
|
2482
|
+
#
|
|
2483
|
+
# The run is over by then: the result has been returned (or is about to
|
|
2484
|
+
# be, when no transaction is open), so the hook cannot change it, a
|
|
2485
|
+
# halting verb only stops the remaining +after_commit+ hooks, and the
|
|
2486
|
+
# hook is not recorded in the history. An exception it raises propagates
|
|
2487
|
+
# from wherever the commit happens: the outer +transaction+ call, or
|
|
2488
|
+
# +.run+ itself when no transaction was open.
|
|
2489
|
+
#
|
|
2490
|
+
# _@param_ `target` — the instance method or nested action to run
|
|
2491
|
+
#
|
|
2492
|
+
# _@param_ `options` — step options (+:if+, +:unless+, and for a nested action +:input+)
|
|
2493
|
+
def after_commit: ((Symbol | String | singleton(Actionable::Action)) target, **Object options) -> ::Set[Steps::Base]
|
|
2494
|
+
|
|
2495
|
+
# Hooks that run after the outermost transaction commits a successful run.
|
|
2496
|
+
def after_commit_hooks: () -> ::Set[Steps::Base]
|
|
2497
|
+
|
|
2498
|
+
# {Actionable::Action.describe}, with the +after_commit+ hooks listed as
|
|
2499
|
+
# one more +:hooks+ section, so {Actionable::Action.describe_text} shows
|
|
2500
|
+
# them too (only when there are any, like the core sections).
|
|
2501
|
+
def describe: () -> ::Hash[Symbol, Object]
|
|
2502
|
+
|
|
2503
|
+
# Give each subclass its own copy of the inherited +after_commit+ hooks.
|
|
2504
|
+
#
|
|
2505
|
+
# _@param_ `subclass`
|
|
2506
|
+
def inherited: (Class subclass) -> void
|
|
2507
|
+
|
|
2508
|
+
# Extend the run-start guard (decision D18) to the +after_commit+ hooks.
|
|
2509
|
+
#
|
|
2510
|
+
# _@param_ `instance`
|
|
2511
|
+
def ensure_steps_implemented!: (Action _instance) -> void
|
|
2512
|
+
|
|
2513
|
+
# Extend the recording-verb position check (owner ruling R5) to the
|
|
2514
|
+
# +after_commit+ hooks.
|
|
2515
|
+
def verb_checked_steps: () -> ::Array[Steps::Base]
|
|
2516
|
+
|
|
2517
|
+
def ensure_after_commit_supported!: () -> void
|
|
2518
|
+
end
|
|
2519
|
+
|
|
2520
|
+
# Prepended onto {Actionable::Runner} (outside {RunnerTransaction}) to
|
|
2521
|
+
# register a successful run's +after_commit+ hooks once the run, and its
|
|
2522
|
+
# own transaction, are over.
|
|
2523
|
+
module RunnerAfterCommit
|
|
2524
|
+
def run: () -> Result
|
|
2525
|
+
|
|
2526
|
+
def schedule_after_commit: () -> void
|
|
2527
|
+
|
|
2528
|
+
# Run the hooks in order. A halt stops the remaining ones; nothing they
|
|
2529
|
+
# record reaches the already-returned result.
|
|
2530
|
+
#
|
|
2531
|
+
# _@param_ `hooks`
|
|
2532
|
+
def run_after_commit_hooks: (::Array[Steps::Base] hooks) -> void
|
|
2533
|
+
end
|
|
2534
|
+
|
|
2535
|
+
# Internal signal raised inside a transaction block to trigger a rollback
|
|
2536
|
+
# without surfacing as a genuine error. The runner wrapper swallows it; a
|
|
2537
|
+
# real ActiveRecord transaction rolls back and re-raises it (as it does for
|
|
2538
|
+
# any exception), and the wrapper catches it outside the block.
|
|
2539
|
+
class Rollback < StandardError
|
|
2540
|
+
end
|
|
2541
|
+
|
|
2542
|
+
# The +transactional+ class macro (decision D9). Extended onto
|
|
2543
|
+
# {Actionable::Action} by +actionable/rails+.
|
|
2544
|
+
module Transactions
|
|
2545
|
+
# Wrap this action's run in +model.transaction(**options)+.
|
|
2546
|
+
#
|
|
2547
|
+
# transactional model: :invoice
|
|
2548
|
+
# transactional model: Invoice, requires_new: false # join an outer one
|
|
2549
|
+
#
|
|
2550
|
+
# Run inside a transaction that is already open, the action gets its own
|
|
2551
|
+
# savepoint (+requires_new: true+) unless +requires_new:+ is given, so a
|
|
2552
|
+
# recorded +Failure+ rolls back its own writes rather than leaving them for
|
|
2553
|
+
# the outer transaction to commit.
|
|
2554
|
+
#
|
|
2555
|
+
# _@param_ `model` — the model whose +transaction+ is used; a Symbol is resolved to a constant (+:invoice+ → +Invoice+)
|
|
2556
|
+
#
|
|
2557
|
+
# _@param_ `options` — options forwarded to +transaction+
|
|
2558
|
+
def transactional: (model: (Symbol | Class), **Object options) -> void
|
|
2559
|
+
|
|
2560
|
+
# The transaction config for this action, inherited from ancestors when
|
|
2561
|
+
# not set locally (decision D9: inherited, overridable).
|
|
2562
|
+
#
|
|
2563
|
+
# _@return_ — +{model:, options:}+, or +nil+ when
|
|
2564
|
+
# neither this action nor an ancestor is +transactional+
|
|
2565
|
+
def transaction_config: () -> ::Hash[Symbol, Object]?
|
|
2566
|
+
end
|
|
2567
|
+
|
|
2568
|
+
# Prepended onto {Actionable::Runner} so a +transactional+ action runs its
|
|
2569
|
+
# whole pipeline inside a transaction. A raised exception or a recorded
|
|
2570
|
+
# +Failure+ rolls everything back; a +Success+ commits (decision D9).
|
|
2571
|
+
module RunnerTransaction
|
|
2572
|
+
def run: () -> Result
|
|
2573
|
+
|
|
2574
|
+
# _@return_ — the running action's transaction config
|
|
2575
|
+
def transaction_config: () -> ::Hash[Symbol, Object]?
|
|
2576
|
+
|
|
2577
|
+
# The configured options, plus +requires_new: true+ when a transaction is
|
|
2578
|
+
# already open and the action did not say otherwise. Joining the outer
|
|
2579
|
+
# transaction would swallow this run's rollback, so a recorded failure's
|
|
2580
|
+
# writes would commit with it (decision D9).
|
|
2581
|
+
#
|
|
2582
|
+
# _@param_ `model`
|
|
2583
|
+
#
|
|
2584
|
+
# _@param_ `options`
|
|
2585
|
+
def transaction_options: (Class model, ::Hash[Symbol, Object] options) -> ::Hash[Symbol, Object]
|
|
2586
|
+
|
|
2587
|
+
# _@param_ `model`
|
|
2588
|
+
#
|
|
2589
|
+
# _@return_ — the resolved model class
|
|
2590
|
+
def resolve_model: ((Symbol | Class) model) -> Class
|
|
2591
|
+
end
|
|
2592
|
+
end
|
|
2593
|
+
end
|