Repository navigation
Releases: Paymentbox-com/actionable
Release list
Actionable v1.10.0
Ruby 3.3 or newer is now required; the library itself is unchanged, so on
Ruby 3.3+ this is a drop-in bump from 1.9.0 (UPGRADING.md "1.9.0 → 1.10.0").
The rest is docs: every Ruby block in the docs is now parsed by a spec, USAGE
explains Result#status vs code and struct-typed input errors, and the README
installs from RubyGems. CI runs on our own self-hosted runners.
Changed
- Ruby 3.3 or newer is required (
required_ruby_version >= 3.3.0, was
>= 3.2.0). 3.3 is the oldest Ruby a known client runs, and CI is moving to
self-hosted Ubuntu 26.04 runners. The library code is unchanged; CI drops the
3.2 lane and RuboCop targets 3.3.
Upgrade note: on Ruby 3.2, stay on 1.9.x.
Fixed
- A spec now parses every ```ruby block in README, USAGE, UPGRADING, AGENTS,
the skill anddocs/, doctest or not, so an example can't rot into invalid
Ruby unseen. The one it caught, a `def … = ...` placeholder in UPGRADING's
core-method-name entry, is now real Ruby.
Documentation
- USAGE "Results" says what
statusis and that aSuccess'scodeis
:success; USAGE "Input" shows a FieldStruct class as an input field and how
an invalid one is reported (errorsnames only the field). Both doctested;
gaps found by the docs-site tutorial. - README installs from RubyGems (
gem 'actionable-rb'); 1.9.0 is published there.
Actionable v1.9.0
The gem is renamed actionable-rb so it can be published to rubygems.org,
where actionable belongs to an unrelated 2014 gem. Nothing in the library
changed: require 'actionable', the Actionable module and every API stay the
same. Two lines in an app change: the Gemfile's gem 'actionable' becomes
gem 'actionable-rb', and a .rubocop.yml that loads the cop with plugins:
renames its key to actionable-rb. Read UPGRADING.md "1.8.2 → 1.9.0".
Changed
- The gem is renamed
actionable-rb, so it can be published to rubygems.org,
whereactionablebelongs to an unrelated 2014 gem.require 'actionable',
theActionablemodule and every API are unchanged. A new
lib/actionable-rb.rbletsBundler.requireload it by the gem name, and the
gemspec now pushes to rubygems.org instead of GitHub Packages.
Upgrade note: the Gemfile line becomesgem 'actionable-rb', and a
.rubocop.ymlthat loads the cop withplugins:must use theactionable-rb
key.
Actionable v1.8.2 — review fixes
Three fixes found by a mutation-driven review, plus upgrade-guide answers from
a dry run against a large app. All three change what an app can see, so read
UPGRADING.md "1.8.1 → 1.8.2" before bumping. case_step branches now honour
if: / unless:: a guarded-out branch does nothing, silently, where the guard
used to be ignored. Actionable::RBS generates valid class names for ? / !
codes and raises on colliding ones. The first-run verb check sees through
alias_method, so it can raise on an aliased step it used to skip.
Fixed
case_stepbranches honourif:/unless:.on 'x', :handle, if: :ready?(anddefault :y, unless: …) accepted the guard and then ignored
it, so the branch always ran. The branch is still picked by value alone; its
guard now decides whether the target runs, as for a plain step. A matched
branch whose guard opts out does nothing: no fall-through to the next branch
or thedefault. Upgrade note: a branch you guarded now really skips.Actionable::RBSnames output classes for?/!codes validly.
Undertyped_outcomes,fails(:expired?) { … }emittedclass Expired?FailureOutput, which RBS can't parse. A trailing?/!now
becomesPredicate/Bang(ExpiredPredicateFailureOutput,
DoneBangSkipOutput), so:xand:x?get different classes.generate
now raisesArgumentErrorwhen two codes of one kind would share a class
name (:ready/:ready_), which silently emitted one class for two
schemas, or when a code can't start a class name. Upgrade note: rename
one of two clashing codes.- The first-run verb check sees through
alias_method. A step naming an
alias (step :chargewithalias_method :charge, :do_charge) looked up
the def by the alias name, found none, and skipped the check; it now reads
the originaldef, so a misplaced verb there raisesDefinitionErrorlike
any other step. Documented limit: a moduleprepended over a step method
hides the class's owndeffrom the check. Upgrade note: fix the
reported verb, as for any verb-position offense.
Documentation
- UPGRADING.md, from a dry run against a large app (133 actions). The guide
now says which six Finds must run on a green suite (silent behaviour changes)
and that the rest cover code the suite doesn't reach. ThedescribeFind skips
RSpec.describe(1,552 hits before). The code Finds skip receivers and
non-Ruby files. The@resultentry says aResultyou build yourself is still
allowed, withhalt!. A new section lists the class-level API unchanged since
1.5.1. The cop entry says to list every base class defined in its own file. The
opt-out goes inspec_helper, not an initializer. The effort estimate counts
suite runs, and there is a PCRE2 check. AGENTS.md and the skill follow.
Actionable v1.8.1 — housekeeping
Housekeeping. The Actionable/VerbNotLast cop resolves superclass names the way
Ruby does, so same-named classes in one file no longer confuse it; the
UPGRADING.md drift guard is fixture-tested and now asks for an explicit
decision on every visible CHANGELOG entry. No runtime behaviour changes.
Changed
Actionable/VerbNotLastresolves a superclass by its lexical full name.
It matched by the last name segment, so in one filemodule B; class Base; end; class X < Basecounted as an action whenmodule Aheld aBase < Actionable::Action, and a file's ownLegacy::ApplicationActioncounted as
the listed base. It now looks the name up as Ruby does (enclosingmodule/
classoutwards, a compactclass A::Bopens onlyA::B,::Xis the
top level), so those classes are left alone, and a listed full name is
found from inside its namespace (module Ops; class X < BaseActionwith
Ops::BaseActionlisted). An offense it newly reports is a call the
first-run check already raises on.
Tests / CI
- The UPGRADING.md drift guard asks for a decision on every visible
change. Each[Unreleased]bullet under### Changed,### Removedor
### Breaking…must carry anupgrading: <slug>marker comment (with its
guide entry, as before) orupgrading: none, so a change filed there
without an upgrade note no longer slips past it. Released sections are not
re-checked. The guard's own examples now run on fixtures, so they hold in
any release state; one example still checks the live files.
Actionable v1.8.0 — verb guard and upgrade guide
A guard against silently continuing runs, plus upgrade guidance. A plain
fail / fail_with / skip / succeed must now be the last thing its step
or hook does — checked once per action class on first run (Prism; turn it off
with Actionable.verb_position_check = false) and by a shipped RuboCop cop,
Actionable/VerbNotLast. fail / skip codes must be identifiers, so the
fail 'card expired' Kernel habit raises instead of recording a code. Both can
raise on code that used to run: read UPGRADING.md, which walks 1.5 → 1.8
change by change. CI now also runs real ActiveJob/Sidekiq and ActiveRecord 7.1
lanes. Still requires field_struct >= 0.12.
Added
- A plain recording verb must end its step or hook method (owner rulings
R5, R10).fail :card_expired if card.expired?followed by more code keeps
running — almost always afail!missing its bang. On an action's first
run, every step method (case-step branch targets included) and lifecycle
hook method (on_success/on_failure/on_skip/always, and
after_commitwith the Rails adapter) is parsed with Prism, and a plain
fail/fail_with/skip/succeed(orself.fail…) that isn't the
method's last expression raisesDefinitionErrornaming the action, method,file:line,
verb and fix. Branches,return fail(...), a verb followed byreturn,
and blocks whose call is last are allowed; USAGE (Control flow, "Where a
plain verb may go") has the full table. Checked once per class: later runs
pay one instance-variable read. Skipped without Prism >= 1.0 (Ruby 3.2
without theprismgem; no new dependency) and for methods with no
parseable source (eval,define_method). Actionable.verb_position_check = falseturns that first-run check off
for the process (on by default;Actionable.verb_position_check?reads it).
Off,runnever parses source or loads Prism. It is an escape hatch while
upgrading, and leaves the cop as the only guard. The identifier check on
fail/skipcodes stays on: it's a per-call regex, not a parse.Actionable/VerbNotLast, an opt-in RuboCop cop with the same rules
(one shared analyzer). Not loaded byrequire 'actionable'; enable it with
plugins: [{actionable: {require_path: actionable/rubocop}}](RuboCop >=
1.72, via the gem's lint_roller plugin) orrequire: [actionable/rubocop].
It checks a class whose superclass is an action base and that declares
step/case_step, and the methods that class names in its declarations.
An action base is one of the cop'sActionBaseClasses(default
Actionable::Action,ApplicationAction) or a class defined in the same
file whose superclass chain reaches one, so a dry-transaction class with
its ownstepgets no false positives (owner ruling R10).rake benchgainsfail! actionandskip! actionscenarios.
Changed
fail/skipcodes must be identifiers. A code that isn't a Symbol
or String matching/\A[[:alpha:]_][[:alnum:]_]*[?!]?\z/('card expired',:"not-found",'payment.declined', a class) raises
ArgumentErrorat the call, suggestingraiseforKernel#failhabit.
Applies tofail/fail!/fail_with/fail_with!/skip/skip!.
Upgrading: both checks raise on code that used to run.
DefinitionError"… callsfailwith more code after it …" on an action's
first run: if the code after the verb should not run, use the bang verb
(fail!); if it should, move the verb to the end of the method (or
return fail(...)). Run the cop over your app to find them all up front.
If you can't fix them all before deploying, set
Actionable.verb_position_check = falsein an initializer until you have.ArgumentError"… code "…" is not an identifier": use a Symbol code and
pass the sentence as the message (fail(:card_expired, 'card expired')),
orraiseif you meant an exception.
Tests / CI
- Real-backend lanes. Two new CI jobs feed the required
cicheck:
jobs (ActiveJob + Sidekiq)runs the suite withgemfiles/jobs.gemfile, so
spec/actionable/job_backends/provesJob+honor_retry_metadataon a
real ActiveJob (retry_on Actionable::Job::RetryableFailure:retry: true
re-enqueues until attempts run out,retry: falsediscards or acks, the
default is unchanged) and thatRetryableFailurepropagates out of a Sidekiq
job in inline testing mode (no Redis).activerecord 7.1runs the suite on
ActiveRecord 7.1 (gemfiles/activerecord_7_1.gemfile, Ruby 3.3):
transactionaland savepoints work there, and declaringafter_commit
raises theDefinitionErrornaming 7.2. These specs skip in the default
suite, where the gems or versions they need are absent.
Documentation
UPGRADING.md: how to move an app from 1.5.x to 1.8.0 (owner ruling
R9). A TL;DR with the order of work (field_struct first, then actionable,
the cop, RBS), a straight 1.5-to-1.8 path, a symptom index, and a Symptom /
Find / Fix / Why entry for every change a caller can see, each Find anrg
pattern checked against its "before" code or the tool that finds it. AGENTS.md
and the skill gain a numbered upgrade procedure for agents; README and
getting_started link to it, and the gem ships it.spec/upgrading_spec.rb
keeps it honest: every CHANGELOG upgrade note since 1.5.2 carries an
invisibleupgrading:slug comment that the guide must carry in that
version's section, and the guide's declared range must reach the next
release. Its runnable examples are doctested.- README "Types (RBS)" says the adapter signatures (
transactional,
after_commit, the RSpec helpers,Job) are declared onActionable::Action
even whenactionable/railsisn't required: RBS can't make a signature
conditional andrbsloads a gem's wholesig/.
Actionable v1.7.0 — lifecycle and adapter hardening
Lifecycle and adapter hardening. always hooks now run even after an outcome
hook halts; redeclaring a step or hook overrides it in place; the Rails adapter
gains an opt-in after_commit hook (ActiveRecord >= 7.2) that runs only after
the outermost transaction commits; Job can opt in to honouring a failure's or
skip's declared retry:; the Rails, Job and RSpec surfaces now ship RBS. Two
behaviour changes have upgrade notes under Changed. Still requires
field_struct >= 0.12.
Added
benchmarks/andrake bench. A dependency-free script (monotonic
clock, nobenchmark/ benchmark-ips) times a trivial run, a 5-step run with
measure :noneand:all, 5-field input coercion, a nested action with and
without measurement, andhistory.to_json.BENCH_ITERATIONSsets the loop
count. Not in the default task and not packaged in the gem; a smoke spec
runs it with 5 iterations so it can't rot. README has a "Performance"
section with indicative numbers.
Documentation
-
failis notraiseinside an action. USAGE (Control flow), README,
getting_started, AGENTS.md and the skill now warn that the gem'sfailverb
replacesKernel#failin an action's instance methods:fail 'boom'
records aFailurewith code:"boom"and the run continues, and
fail SomeError, 'msg'raisesNoMethodError. Useraisefor exceptions
andfail!(:code)to stop with a failure. Behaviour is unchanged. -
README, getting_started, AGENTS.md and the skill now mention the 1.7.0
topics where they list their neighbours: step/hook redeclaration (last
wins),alwayshooks running after a hook halt,after_commit, and
honor_retry_metadata, each linking to USAGE. -
RBS for the adapters' public surface (owner ruling P11).
sig/now
declares what clients call from the adapters:transactional/
transaction_configandafter_commit/after_commit_hookson
Actionable::Action(Rails adapter), and the RSpec matchers
(perform_actionableand its chain) and stubs (allow_actionable_*,
stub_actionable_*,actionable_*_result). They are generated from YARD by
sord like the core's (.yardoptsnow excludes onlyProxyValidator, the
Railtie andactionable/rspec/coverage); the one runtime mix-in YARD cannot
see,after_commit's singleton-class prepend, is declared by hand in
sig/actionable/rails.rbs. A spec compares each adapter type's public
methods, by reflection, with its RBS so neither can drift.Actionable::Job
was already generated. The report-only Steep lane now checks the adapters
too. -
describe/describe_textlistafter_commithooks. With the Rails
adapter loaded,describe[:hooks][:after_commit]names them, and
describe_textadds anafter_commit:hooks line when there are any. -
after_commithook kind (Rails adapter). Afterrequire 'actionable/rails',
after_commit :method(or a nested action, with:if/:unless/
:inputlike any hook) runs a side effect only after the database work
really committed. It is registered with
ActiveRecord.after_all_transactions_commitwhen a run ends in aSuccess,
so it waits for the outermost open transaction (not the action's own
savepoint), runs immediately when none is open, and never runs if an
enclosing transaction rolls back. AFailureorSkippednever runs it.
The run is over by then: the hook can't change the result, isn't recorded in
the history, and an exception it raises propagates after the commit (from
the outertransactioncall, or.runwhen no transaction was open).
Needs ActiveRecord >= 7.2; declaring it on an older one (or without
ActiveRecord loaded) raises aDefinitionError. Existing hooks and the
core are unchanged. -
honor_retry_metadata: letActionable::Jobact on declaredretry:
metadata. An action that calls it (the macro comes with
require 'actionable/job') has theretry:key of its declared failures and
skips decide the job's disposition:retry: trueretries (a skip or a
permanent-code failure included) andretry: falsedoes not (a failure is
discarded, a skip acks). Codes without the key map as before. Retried skips
raise the sameRetryableFailure, carrying theSkipped. Inherited; a
subclass backs out withhonor_retry_metadata(enabled: false).
Actionable::Job.dispositiontakes the matchinghonor_retry:keyword.
Off by default, so metadata stays neutral (D20/D21) and existing jobs behave
exactly as before.
Changed
-
Redeclaring a step or hook now overrides it. A second
step :x,
case_step :x, nestedstep SomeAction, oron_success/on_failure/
on_skip/always :xwith the same type and name, in the same class or a
subclass, replaces the earlier declaration's options (guards,:input/
:expose, case branches) and keeps the earlier position. Before, the
redeclaration was silently dropped and the first declaration's options won,
so a subclass could not override an inherited step as invariant 5 promises.
A subclass's override never changes the parent. Upgrade note: an action
that redeclares a step with different options now runs the last options. -
alwayshooks now run even after an outcome hook halts. A bang verb
(fail!/skip!/succeed!),halt!, or a failing nested action inside
anon_success/on_failure/on_skiphook still has its result adopted
and still stops the remaining hooks of that category, but it no longer skips
thealwayshooks: they run next and see the adopted result. Before, one
halt stopped every remaining hook, so cleanup and audit hooks could be
silently skipped. A halt inside analwayshook is adopted the same way and
stops only the remainingalwayshooks. Undertransactional,always
hooks still run inside the transaction and the adopted result still decides
commit or rollback, so analwayshook's writes roll back with a failed run.
When measuring, thosealwayshooks appear in the history as usual.
Upgrade note: analwayshook that assumed it would not run after a hook
failure must now checkresultitself.
Actionable v1.6.0 — valid, guarded RBS
Valid, guarded RBS. The gem's own signatures can only get more typed (CI
ratchet, untyped 46 → 27); generated RBS for your actions declares the real
superclass chain, a typed InputSchema, typed .run keywords and output
ivars, built on field_struct 0.12's FieldStruct::RBS.type_for. Requires
field_struct >= 0.12. No runtime behaviour changes.
Added
Actionable::RBS.generatetypes an action's input and output instance
variables (generated RBS only; no runtime change). For an action with a
fixedinputschema it emits a nestedclass InputSchema < ::FieldStruct::Base
with a typed reader and writer per input field,def input: () -> InputSchema,
and a.runoverload| (InputSchema input) -> ...beside the keyword form.
InputSchemaexists only in RBS (the runtime schema is anonymous), so
generation raisesArgumentErrorwhen Ruby already resolves
YourAction::InputSchema(a constant on the action, an ancestor or an
included module). It also declares each output field as a nullable
instance variable (@total: ::Integer?), so a step that assigns
@total = ...type-checks. Upgrade note: the generated RBS now owns
those instance variables; delete any declaration of them from your own sig
files, since RBS rejects an instance variable declared twice.
Fixed
Actionable::RBS.generatedeclares the real superclass chain. An action
under an intermediate base (class Codes::DecideMapping < ApplicationAction)
was declared< ::Actionable::Action, hiding its bases from the type
checker, and a host that declared the real superclass got
NoSuperclassFoundError/ a failingrbs validatewhen the base had no
signature. The action now names its nearest named superclass, and every
named ancestor up toActionable::Actionis declared first, member-less
(a typed one with its empty result classes), so the RBS resolves on its own.
The declarations repeat byte-identically across files and merge. A namespace
that is a class now opens asclass, notmodule, which RBS rejected as a
duplicate declaration.
Upgrade note: if your own sig files declare one of those bases with a
different superclass (e.g.class ApplicationAction < ::Actionable::Action
while Ruby says otherwise), RBS now reports a superclass mismatch; make your
declaration match Ruby's ancestry or drop it.
Changed
- Optional
.runkeywords are nullable in generated RBS (?note: ::String?): an optional field may be passednilexplicitly, and the run succeeds. - A required input field with a default is an optional
.runkeyword (?amount: ::Integer), sincerunsucceeds without it. An action whose input is chosen by aninput_fordispatch (including one inherited under a fixedinput) gets.run: (*untyped), since the dispatch decides the keywords at run time. - Requires field_struct >= 0.12. The generator now takes every field type
fromFieldStruct::RBS.type_forinstead of its own copy of the mapping, so
generated action RBS matchesFieldStruct::RBS.generate. Consumer-visible
changes to generated RBS: writers onOutputand the per-code output
classes are typed(T? value) -> T?instead of(untyped value) -> untyped; scalar array elements are nullable (::Array[::String?]); a
:valuefield isuntyped, neveruntyped?; a bare genericruby_type
gets type arguments (::Hash[untyped, untyped]); and a field'srbs_type:
override is honoured. A field typed by an anonymous nested struct class now
raisesArgumentError(it previously emitted an invalid::name). - The committed signatures can only get more typed. Contributor tooling
only; the shippedsig/actionable.rbsis unchanged.rake sigs:untyped
(part of the default task) fails when sig/ holds more or fewer than the
committed ceiling of 46 non-commentuntyped, so a typing change lowers
the ceiling with it.sigs:generate/sigs:checkfail on a sord
[OMIT]/[DUCK]fallback that isn't in the committed allowlist (24
today) and on allowlist entries sord no longer emits. sord runs without
--replace-errors-with-untyped(the output is identical) and fails on any
SORD_ERROR_type.sigs:validatevalidates the whole sig/ directory. - Optional, report-only Steep 2.x via
gemfiles/types.gemfileand a
typesCI job on Ruby 3.4 that never fails the build. The Steepfile now
checks the core only (the RSpec and Rails adapters are ignored); Steep is
no longer in the main Gemfile. - More precise signatures (RBS only; no runtime change). Keyword splats
are typed per value (**Object options, not**::Hash[Symbol, Object] options, which RBS reads as "every value is a Hash");.run,#initialize
andJob::Mixin#run_actionabletake*Object, **Object; the gem's
constants (Actionable::HALT,VERSION,Action::GEM_FAILURE_CODES, …)
are declared; and the halt/measurement helpers declare their block. The
DSL methods whose block runs with anotherself(output,input,
fails,skips,case_step,input_for, and thedefinebuilders)
gain a block overload with its self-type in the new
sig/actionable/dsl.rbs.untypedin sig/ falls from 46 to 27, and the
sord fallback allowlist from 24 entries to 4.
v1.5.2 — runner and nested-action fixes
Runner and nested-action fixes: hooks that halt, kwargs that survive hooks,
nested actions that neither wipe parent ivars nor commit failed writes.
Requires field_struct 0.11.0 or later.
Changed
- Requires field_struct >= 0.11. It is the release whose numeric and input
fixes this one is tested against; CI now pins it by tag. - CI runs Ruby 3.2, 3.3, 3.4 and 4.0 (was 3.3.6 only), plus the docs guard,
behind one aggregatecicheck thatmainrequires. - The packaged gem leaves out contributor-only files (
.claude/,
CLAUDE.md,docs/origin/, Rakefile/Steepfile/lint config). It still ships
lib/,sig/, the user docs,AGENTS.mdand the Claude skill.
Fixed
-
A failed
transactionalaction inside an outer transaction no longer
commits its writes. It joined the outer transaction, whose commit ignored
the action's rollback, so a recordedFailurestill committed its rows
(measured on ActiveRecord 8.1 + SQLite). Inside an open transaction the action
now takes its own savepoint (requires_new: true) unlessrequires_new:is
given;requires_new: falsekeeps the old joining behaviour. -
A nested action no longer wipes parent ivars it never set. Absorbing a
successful or skipped child copied every declared output field, so a field
the child left nil (or at its default) overwrote the parent's value. Only
fields the child actually set are absorbed now;expose:still narrows them. -
Private step, hook and
case_stepmethods work. The definition check
already accepted them, but the step then raisedNoMethodError; they are
now called like guards are. A step or hook named after a method the action
only inherits from Ruby's core (step :puts,always :exit) raises
DefinitionErrorup front instead of callingKernel. -
Action.inputwithout a block reads the schema instead of replacing it
with an empty one, matchingoutput. -
A step that assigns
@resultgets a clearDefinitionErrorinstead of a
NoMethodErrordeep in the runner. -
History#to_jsonrequires Oj itself rather than relying on another file
having loaded it. -
A bang verb inside a lifecycle hook now sets the result.
fail!,
skip!orsucceed!in a hook used to be dropped:fail!inon_success
returnedSuccess, committed atransactionalaction and acked the job.
The hook's result is now adopted before the halt stops the remaining hooks,
as the docs always said. This includes a nested action used as a hook
(on_success ChildAction): a failing child now fails the run and stops the
remaining hooks. -
succeed/skipkwargs survive lifecycle hooks. After any hook
refreshed the output, the kwargs were lost: a required field set only by
kwarg turned the run intoFailure(:invalid_output), and a kwarg that
overrode an ivar silently reverted to the ivar's value. Present since 1.0.0.
AFailure(:invalid_output)also keeps the output that failed validation
across hooks instead of having it rebuilt from ivars. -
No spurious
errors[:output]on a failure or skip. The best-effort,
unvalidated output of a non-typed failure or skip added
output: ["is invalid"]to the result's errors. It no longer does; errors
the failure recorded itself are kept.
v1.5.1
Added
Coverage.new_gap_hintsets the advice the baseline-mismatch message
gives for a new gap. The default (Coverage::DEFAULT_NEW_GAP_HINT) is
unchanged; a host whose baseline only shrinks can drop "or add it to the
baseline".
Fixed
- RBS for a subclass of a typed action now resolves on its own. Its result
classes subclass the parent's (< ::ApiAuth::BaseAction::Success), which
nothing declared when the parent is an abstract base that is never
generated.RBS.generatenow declares those parent classes, empty, up the
chain of typed ancestors. - RBS readers on
Failureare nullable. Aninvalid_outputfailure
carries the output that broke its schema, so a required field can be
missing; the readers were typed non-nil.
v1.5.0
Typed outcomes (decision D23). Opt-in with typed_outcomes: an action that
doesn't turn it on runs as before, except that a code whose fails or skips
declaration carries a block (new syntax) has its output validated.
Added
typed_outcomesturns typed outcomes on for an action and its
descendants (typed_outcomes enabled: falseturns them off for a subclass);
typed_outcomes?reads it.- A block on
fails/skipsdeclares the output a result with that code
carries, in theoutputDSL.failure_output_schema(code)and
skip_output_schema(code)read it back, anddescribeshows its fields.
Such output is built from the call's keywords alone, never from instance
variables, and validated: output that breaks the schema ends the run as
invalid_output. The block also ends instance-variable capture for that
code, typed outcomes or not. output:onfail/fail!/fail_with/fail_with!, under typed
outcomes only: the failure's output, a Hash or a FieldStruct
(ArgumentErrorotherwise). Without themoutput:onfailis still an
error, andfail_withrefuses it. Under typed outcomes, output for a code
with no schema raisesDefinitionError, naming the code.- Per-action result classes. Under typed outcomes
.runreturns the
action's ownSuccess,FailureorSkipped, each a subclass of the gem
class (or of the parent action's own). A nested failure becomes the parent's
ownFailure, judged by the parent's schema for its code; a parent without
typed outcomes gets a plainActionable::Failure. The RSpec stub helpers
return them too, with output shaped through the code's schema (or empty
without one) as a real run would. - RBS:
Actionable::RBS.generatetypes a typed action's.runas
(Success | Skipped | Failure), with eachcodeandoutputnarrowed and
the output's readers declared on each class. A host's own result subclass,
which.runstill returns as it is, is not described: a known gap.
Changed
- Under typed outcomes, a skip or failure whose code has no schema carries an
empty output, where it used to carry a best-effort capture into the success
schema. The gem's owninvalid_input/invalid_outputkeep theirs. - Under typed outcomes, results compare by class:
Act::Successis not==
to anActionable::Successwith the same output, and the stub helpers
return a new object of the action's own class. Specs that compare results
with==need to compare against the action's class.