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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 45242641b6c3a755cb601f0ee6307381e641775560e94bc710eb75dd8f2959dc
|
|
4
|
+
data.tar.gz: 4d914d784d7066d12a4240842ba4daef80ce8fa11276d7c15a4c731a6f2ea6ef
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 1ec848dc0cdef6029a58719528263875682085fdc37f4200711581efccf92a971270271dde8abed11d8c17caf5072223109a061bc68a103097319624caec375d
|
|
7
|
+
data.tar.gz: ade35f4c5112e57198169a4088f407e6249e37b56c4801dccd7beda0bed870b7807afaf7cb65c467812f3bed96a4a16e226739c0b45a591464a635cc2ff50837
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "actionable",
|
|
3
|
+
"owner": {
|
|
4
|
+
"name": "Adrian Madrid",
|
|
5
|
+
"email": "amadrid@pmtbox.com"
|
|
6
|
+
},
|
|
7
|
+
"plugins": [
|
|
8
|
+
{
|
|
9
|
+
"name": "actionable",
|
|
10
|
+
"source": ".",
|
|
11
|
+
"description": "Guidance for building, debugging, and testing Actionable service objects (the actionable gem's typed step/result DSL).",
|
|
12
|
+
"category": "development"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "actionable",
|
|
3
|
+
"description": "Guidance for building, debugging, and testing Actionable service objects — the actionable gem's typed step/result DSL.",
|
|
4
|
+
"author": {
|
|
5
|
+
"name": "Adrian Madrid",
|
|
6
|
+
"email": "amadrid@pmtbox.com"
|
|
7
|
+
},
|
|
8
|
+
"homepage": "https://github.com/Paymentbox-com/actionable"
|
|
9
|
+
}
|
data/.rspec
ADDED
data/AGENTS.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Working with the actionable gem
|
|
2
|
+
|
|
3
|
+
This repo provides **Actionable** — a Ruby gem for service objects: subclass
|
|
4
|
+
`Actionable::Action`, declare ordered **steps**, implement each as a method, and run
|
|
5
|
+
it to get a single typed **result**. This file routes AI coding agents to the right
|
|
6
|
+
docs and the things most often gotten wrong. (For *contributing to the gem itself*,
|
|
7
|
+
see `CLAUDE.md`.)
|
|
8
|
+
|
|
9
|
+
## Read these
|
|
10
|
+
|
|
11
|
+
- **[`docs/getting_started.md`](docs/getting_started.md)** — how to use it in a repo
|
|
12
|
+
(Ruby + Rails): a task→API map, an error→fix table, and a worked example.
|
|
13
|
+
- **[`USAGE.md`](USAGE.md)** — dense reference: every step type, option, and macro.
|
|
14
|
+
- **[`README.md`](README.md)** — feature overview.
|
|
15
|
+
- `skills/actionable/SKILL.md` — the bundled Claude Code skill. Install it with
|
|
16
|
+
`/plugin marketplace add Paymentbox-com/actionable` then
|
|
17
|
+
`/plugin install actionable@actionable` (the repo is a single-plugin marketplace).
|
|
18
|
+
|
|
19
|
+
## The shape
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
class CreateInvoice < Actionable::Action
|
|
23
|
+
input { required :amount, :big_decimal; optional :name, :string }
|
|
24
|
+
output { required :invoice, Invoice }
|
|
25
|
+
|
|
26
|
+
step :build
|
|
27
|
+
step :persist
|
|
28
|
+
|
|
29
|
+
def build = @invoice = Invoice.new(amount: input.amount, name: input.name)
|
|
30
|
+
def persist = @invoice.save!
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
result = CreateInvoice.run(amount: 19.99, name: 'Acme')
|
|
34
|
+
result.success? # => true
|
|
35
|
+
result.code # => :success
|
|
36
|
+
result.output.invoice # => #<Invoice ...> (typed)
|
|
37
|
+
result.invoice # => same, via convenience delegation
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## The gotchas
|
|
41
|
+
|
|
42
|
+
1. **`fail`/`succeed` vs `fail!`/`succeed!`.** The plain versions *record* an outcome
|
|
43
|
+
and keep running; the bang versions *also stop* the pipeline (`halt!` stops without
|
|
44
|
+
recording). Last write wins; with no call, the action auto-succeeds.
|
|
45
|
+
**`fail` is not `raise` here:** inside an action it is the gem's verb, so
|
|
46
|
+
`fail 'boom'` records a `Failure` with code `:boom` and keeps running (a
|
|
47
|
+
sentence code like `'card expired'` raises `ArgumentError`). Use `raise` for
|
|
48
|
+
a bug, `fail!(:code, 'msg')` to stop with a failure.
|
|
49
|
+
**A plain verb must be the last expression of its step/hook method.**
|
|
50
|
+
`fail :x if bad?` followed by more code raises `DefinitionError` on the
|
|
51
|
+
action's first run: write `fail!` to stop there, or move the verb last
|
|
52
|
+
(`return fail(:x) if bad?` also works). Lint it with the opt-in
|
|
53
|
+
`Actionable/VerbNotLast` cop (`require_path: actionable/rubocop`). See
|
|
54
|
+
[USAGE, Control flow](USAGE.md#where-a-plain-verb-may-go).
|
|
55
|
+
2. **Output is declared, not magic.** Only fields in `output do … end` appear on the
|
|
56
|
+
result. Setting an undeclared `@foo` does nothing for the caller — declare it.
|
|
57
|
+
3. **Genuine exceptions propagate.** A `raise` inside a step is *not* turned into a
|
|
58
|
+
failure. Catch it yourself, or `fail`/`fail!` deliberately. Assert on it in specs
|
|
59
|
+
with `perform_actionable(...).and_raise(SomeError)`.
|
|
60
|
+
4. **Transactions need the Rails adapter.** `transactional model: :invoice` only
|
|
61
|
+
works after `require 'actionable/rails'` (auto-loaded when ActiveRecord is present).
|
|
62
|
+
The core never wraps a transaction on its own. The same goes for
|
|
63
|
+
`after_commit :method` (ActiveRecord >= 7.2): it runs a successful run's side
|
|
64
|
+
effect only after the outermost transaction commits — use it, not `on_success`,
|
|
65
|
+
for emails/webhooks/enqueues from a transactional action.
|
|
66
|
+
5. **Nested actions thread values explicitly.** `step Child, input: %i[invoice]`
|
|
67
|
+
passes `invoice` in; `expose: %i[receipt]` limits what comes back. Without
|
|
68
|
+
`expose:`, all of the child's outputs are absorbed.
|
|
69
|
+
6. **A skip is `ok?`, not a failure — and now carries output.** `skip(code, msg,
|
|
70
|
+
**output)` mirrors `succeed` (captured best-effort, unvalidated), so an
|
|
71
|
+
idempotent hit can return the existing record's id. Don't model "already done"
|
|
72
|
+
as a `fail`.
|
|
73
|
+
7. **Declared failures are strict.** Once an action says `fails :refused, kind:
|
|
74
|
+
:rejected`, every code it fails with must be declared, or `fail` raises
|
|
75
|
+
`DefinitionError`. `:invalid_input` / `:invalid_output` are exempt. The
|
|
76
|
+
metadata after the message is the app's, read back as `result.metadata`;
|
|
77
|
+
`fail`'s keywords are only ever errors.
|
|
78
|
+
8. **Declared skips are strict too, the default included.** Once an action says
|
|
79
|
+
`skips :not_ready`, every `skip` / `skip!` must use a declared code, or it
|
|
80
|
+
raises `DefinitionError`. Nothing is exempt, so a bare `skip!` (code
|
|
81
|
+
`:skipped`) on a declaring action needs `skips :skipped`. A child's skip
|
|
82
|
+
continues the parent, so a parent never declares its children's skip codes.
|
|
83
|
+
9. **Redeclaring overrides, last wins.** A second `step :x` (or `case_step`,
|
|
84
|
+
nested `step Child`, or hook) with the same name, in the class or a
|
|
85
|
+
subclass, replaces the earlier options and keeps the earlier position. You
|
|
86
|
+
can't remove an inherited step; guard it with `if:` / `unless:`.
|
|
87
|
+
10. **`always` hooks run even after a hook halts.** A `fail!` in `on_success`
|
|
88
|
+
stops the rest of the outcome hooks and becomes the result, but the
|
|
89
|
+
`always` hooks still run and see it. Put cleanup and audit in `always`;
|
|
90
|
+
check `result` there if the outcome matters.
|
|
91
|
+
|
|
92
|
+
## Fast paths
|
|
93
|
+
|
|
94
|
+
- Declare what an action can fail with: `fails :not_found, kind: :not_found`;
|
|
95
|
+
read it back with `Action.declared_failures` or off the result as `result.metadata`.
|
|
96
|
+
- Declare what an action can skip with: `skips :not_ready, retry: true`; read it
|
|
97
|
+
back with `Action.declared_skips` or off the skipped result as `result.metadata`.
|
|
98
|
+
- See an action's contract without reading source: `pp CreateInvoice.describe`
|
|
99
|
+
(or `CreateInvoice.describe_text` for a readable summary).
|
|
100
|
+
- Run over a collection: `Action.run_each(items) { |i| {…} }` → a `BatchResult`
|
|
101
|
+
with `all_ok?` / `any_failure?` / `partial?` and `to_api_h`.
|
|
102
|
+
- Pick the input schema at runtime: `input_for(:event_type) { on 'sale', SaleShape; default … }`.
|
|
103
|
+
- Validate a side struct and fail with its errors: `fail_with(EventShape.new(payload), code: :invalid_event)`.
|
|
104
|
+
- Render a result for an HTTP response: `result.to_h` / `result.to_api_h(index:)`.
|
|
105
|
+
- Run an action from a background job: `include Actionable::Job::Mixin` and call
|
|
106
|
+
`run_actionable(...)` in `perform` (requires `actionable/job`). Add
|
|
107
|
+
`honor_retry_metadata` to the action to make declared `retry: true` /
|
|
108
|
+
`retry: false` metadata decide whether the job retries (off by default).
|
|
109
|
+
- Type your actions for Steep/Solargraph: `Actionable::RBS.generate(CreateInvoice)`.
|
|
110
|
+
- Test a caller without running the action: `stub_actionable_success CreateInvoice, invoice: an_invoice`
|
|
111
|
+
(requires `actionable/rspec`).
|
|
112
|
+
- Find outcomes no spec reaches: `require 'actionable/rspec/coverage'` in a spec
|
|
113
|
+
support file. The suite ends with a table of each action's never-reached
|
|
114
|
+
outcomes; a stubbed result doesn't count. `Coverage.baseline = path` makes CI
|
|
115
|
+
fail on a new gap or a stale entry (see USAGE, "Outcome coverage").
|
|
116
|
+
- Profile a run: declare `measure :all` (or `:sampled, rate: 0.1` in prod), then
|
|
117
|
+
read `result.history.to_json`.
|
|
118
|
+
|
|
119
|
+
## Upgrading
|
|
120
|
+
|
|
121
|
+
Bumping actionable in a host app? Work from [`UPGRADING.md`](UPGRADING.md), not
|
|
122
|
+
from memory: it has a Symptom / Find / Fix / Why entry for every change a caller
|
|
123
|
+
can see since 1.5.x. Procedure:
|
|
124
|
+
|
|
125
|
+
1. **From / to.** Read the current version from `Gemfile.lock` (`actionable
|
|
126
|
+
(x.y.z)`) and pick the target tag. Read UPGRADING.md's TL;DR and only the
|
|
127
|
+
`## A → B` sections between the two.
|
|
128
|
+
2. **field_struct first.** If the target needs a newer one (>= 0.11 for 1.5.2,
|
|
129
|
+
>= 0.12 from 1.6.0), bump it, follow its own UPGRADING.md, and get the suite
|
|
130
|
+
green before touching actionable.
|
|
131
|
+
3. **Bump actionable.** Check `rg --pcre2-version` works in your shell. Run the
|
|
132
|
+
**Find** of each entry UPGRADING.md lists under "Which Finds to run" (they
|
|
133
|
+
change behaviour without raising), even if nothing failed. Run the others for
|
|
134
|
+
code the suite doesn't reach. Keep the hits per entry.
|
|
135
|
+
4. **Enable the cop** (`plugins: [{actionable-rb: {require_path: actionable/rubocop}}]`
|
|
136
|
+
in `.rubocop.yml`; list in `ActionBaseClasses` the two defaults and every app
|
|
137
|
+
base class defined in its own file, by full name, intermediate ones too) and run
|
|
138
|
+
`bundle exec rubocop --only Actionable/VerbNotLast`.
|
|
139
|
+
5. **Apply each entry's Fix.** `Actionable.verb_position_check = false` is only
|
|
140
|
+
for a temporary green baseline: set it in `spec_helper`, not an initializer
|
|
141
|
+
(an initializer switches it off in production too), and remove it before you
|
|
142
|
+
finish. The identifier
|
|
143
|
+
code check has no switch.
|
|
144
|
+
6. **Regenerate RBS** the app owns with `Actionable::RBS.generate`, then run
|
|
145
|
+
`rbs validate` / Steep if the app uses them.
|
|
146
|
+
7. **Run the full suite with the check on.**
|
|
147
|
+
8. **Report** the entries applied, hits left unchanged and why, the entries
|
|
148
|
+
whose Find says no reliable pattern exists (say what you reviewed by hand),
|
|
149
|
+
and the residual risk: behaviour changes the suite may not exercise
|
|
150
|
+
(savepoints, bang verbs in hooks, `always` after a halt, redeclared steps).
|