hubbado-sequence 0.5.0 → 0.6.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 +4 -4
- data/CHANGELOG.md +91 -0
- data/README.md +77 -11
- data/hubbado-sequence.gemspec +1 -1
- data/lib/hubbado/sequence/macros/contract/build.rb +1 -1
- data/lib/hubbado/sequence/macros/contract/deserialize.rb +1 -1
- data/lib/hubbado/sequence/macros/contract/persist.rb +2 -2
- data/lib/hubbado/sequence/macros/contract/validate.rb +2 -2
- data/lib/hubbado/sequence/macros/model/build.rb +1 -1
- data/lib/hubbado/sequence/macros/model/find.rb +2 -2
- data/lib/hubbado/sequence/macros/policy/check.rb +3 -5
- data/lib/hubbado/sequence/pipeline.rb +3 -6
- data/lib/hubbado/sequence/result.rb +59 -20
- data/lib/hubbado/sequence/runner.rb +37 -18
- data/lib/hubbado/sequence/sequencer.rb +3 -2
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cc373389ca48751e5d60ebc84c00dfd8740b437ec89a53b8d9a1b986ea5c2a48
|
|
4
|
+
data.tar.gz: 7053947aa597354f5b973832083ebc8cc9e60e4de3a1cee23524045f803a63cd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 240dbbf380bea6a9007872456c97d052a6cedf0f5b716fbffe63876362f7c66f89f18accbf51d1cd99f2ee0f3f377376c7161770e46d85f6314548bc07d6549b
|
|
7
|
+
data.tar.gz: ef08b3226afc5465ecac7030ca8e36ad9c3a2ecb967530144bb96aad79a670e2085eda9e74d80b4cc05e4617deab78ac0903150ff5ed6b78f4cf1b58a912d778
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,97 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
|
5
5
|
and this project adheres to [Semantic Versioning](http://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [0.6.0] - Result.failure flat kwargs; Dispatch delegates reads and exposes raise helpers
|
|
8
|
+
|
|
9
|
+
### Changed (breaking)
|
|
10
|
+
|
|
11
|
+
- **`Result.failure` takes flat kwargs; the `error:` hash wrapper is
|
|
12
|
+
gone.** The previous shape — `Result.failure(ctx, error: { code:,
|
|
13
|
+
data:, ... })` — wrapped its keys in an `error:` hash for no reason
|
|
14
|
+
beyond convention. The fields are now first-class kwargs on
|
|
15
|
+
`Result.failure` and first-class attrs on `Result`:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
# before
|
|
19
|
+
Result.failure(ctx, error: { code: :forbidden, data: { policy_result: pr } })
|
|
20
|
+
result.error[:code] # => :forbidden
|
|
21
|
+
result.error[:data][:policy_result]
|
|
22
|
+
|
|
23
|
+
# after
|
|
24
|
+
Result.failure(ctx, code: :forbidden, data: { policy_result: pr })
|
|
25
|
+
result.code # => :forbidden
|
|
26
|
+
result.data[:policy_result]
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`Result` exposes `code`, `data`, `step`, `message_override`,
|
|
30
|
+
`i18n_scope`, `i18n_key`, `i18n_args` as readers. `Result#error`
|
|
31
|
+
is removed.
|
|
32
|
+
|
|
33
|
+
Migration: in callers, replace `Result.failure(ctx, error: { code:
|
|
34
|
+
:X })` with `Result.failure(ctx, code: :X)`. Replace `result.error[:X]`
|
|
35
|
+
reads with `result.X`. The `Sequencer#failure(ctx, **error_attrs)`
|
|
36
|
+
helper is unchanged at the call site (it always took flat kwargs).
|
|
37
|
+
Macro substitutes' `fail_with(**error_attrs)` is unchanged at the call
|
|
38
|
+
site; arbitrary extra attrs that used to live in the error hash should
|
|
39
|
+
move into `data:` (`fail_with(code: :forbidden, data: { reason:
|
|
40
|
+
:not_owner })`).
|
|
41
|
+
|
|
42
|
+
- **The per-error `i18n_scope` override path is removed.** Previously a
|
|
43
|
+
caller could put `i18n_scope:` inside the `error:` hash *and* pass a
|
|
44
|
+
separate `i18n_scope:` to the surrounding wrapper, with the per-error
|
|
45
|
+
one winning. With flat kwargs there is one `i18n_scope` slot. The
|
|
46
|
+
`Sequencer#failure` helper still applies the sequencer's auto-derived
|
|
47
|
+
scope when the caller doesn't pass one (`error_attrs[:i18n_scope] ||=
|
|
48
|
+
i18n_scope`), preserving the "caller wins" semantics where it matters
|
|
49
|
+
in practice. `Result#with_i18n_scope` (used to apply a scope to an
|
|
50
|
+
already-built Result) is unchanged.
|
|
51
|
+
|
|
52
|
+
- **`Runner::Dispatch#result` is removed.** Master exposed the wrapped
|
|
53
|
+
Result via `attr_reader :result`, which was the source of the
|
|
54
|
+
`result.result.error.dig(...)` four-hop pattern. With the new
|
|
55
|
+
read-through delegations (`code`, `data`, `step`, `message`,
|
|
56
|
+
`successful_steps`, `ctx`) there's no reason for outcome blocks to
|
|
57
|
+
reach into the inner Result. Any caller still doing
|
|
58
|
+
`result.result.X` from inside a `run_sequence` block will now raise
|
|
59
|
+
`NoMethodError`; replace with the matching delegation on the
|
|
60
|
+
dispatch object itself.
|
|
61
|
+
|
|
62
|
+
- **The `message:` kwarg on `Result.failure` is removed.** It set a
|
|
63
|
+
literal-string fallback returned by `Result#message` when no
|
|
64
|
+
translation matched. No in-tree caller used it (the
|
|
65
|
+
i18n-translation chain plus `humanize_code` fallback covered every
|
|
66
|
+
real case), and the path was test-only. If a caller needs a custom
|
|
67
|
+
message they can supply `i18n_key:` and a matching translation, or
|
|
68
|
+
pass a humanizable `code:` symbol.
|
|
69
|
+
|
|
70
|
+
### Added
|
|
71
|
+
|
|
72
|
+
- **`Runner::Dispatch` delegates reads to its wrapped `Result`.** Outcome
|
|
73
|
+
blocks can call `result.code`, `result.data`, `result.message`,
|
|
74
|
+
`result.step`, `result.successful_steps`, `result.ctx` on the
|
|
75
|
+
`Dispatch` object (the block argument) without hopping through an
|
|
76
|
+
inner `.result.` reference. The previous `result.result.error.dig(...)`
|
|
77
|
+
pattern collapses to one read.
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
result.policy_failed do |ctx|
|
|
81
|
+
if result.data[:policy_result].reason == :not_open
|
|
82
|
+
redirect_to public_path(ctx[:job])
|
|
83
|
+
else
|
|
84
|
+
result.raise_policy_failed
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
- **Public raise helpers on `Runner::Dispatch`:** `raise_policy_failed`,
|
|
90
|
+
`raise_not_found`, and `raise_failed`. They produce the same exceptions
|
|
91
|
+
the safety net would raise, but can be called explicitly from inside an
|
|
92
|
+
outcome block — useful when a caller handles some failure cases inline
|
|
93
|
+
and wants the framework's standard escalation for the rest.
|
|
94
|
+
`enforce_safety_nets!` now delegates to the same helpers, so the
|
|
95
|
+
exception shapes stay aligned whether the caller invokes them directly
|
|
96
|
+
or the runner does it automatically.
|
|
97
|
+
|
|
7
98
|
## [0.5.0] - Result vocabulary renamed: success/failure and successful_steps
|
|
8
99
|
|
|
9
100
|
### Changed (breaking)
|
data/README.md
CHANGED
|
@@ -292,14 +292,30 @@ p.invoke(:check_policy, Policies::User, :user, :update)
|
|
|
292
292
|
```
|
|
293
293
|
|
|
294
294
|
The policy class must respond to `.build(current_user, record)`; the
|
|
295
|
-
instance must respond to the action method and return
|
|
296
|
-
`permitted
|
|
295
|
+
instance must respond to the action method and return a
|
|
296
|
+
`Hubbado::Policy::Result`-shaped object (`permitted?`, `denied?`,
|
|
297
|
+
`reason`, `message`).
|
|
297
298
|
|
|
298
299
|
| | |
|
|
299
300
|
|---|---|
|
|
300
301
|
| **Reads** | `ctx[:current_user]`, `ctx[record_key]` |
|
|
301
302
|
| **Writes** | nothing |
|
|
302
|
-
| **Fails** | `:forbidden` when `permitted?` is false; `
|
|
303
|
+
| **Fails** | `:forbidden` when `permitted?` is false; `result.data` carries `{ policy:, policy_result: }` |
|
|
304
|
+
|
|
305
|
+
A controller can branch on the denial reason via `data`:
|
|
306
|
+
|
|
307
|
+
```ruby
|
|
308
|
+
result.policy_failed do |ctx|
|
|
309
|
+
if result.data[:policy_result].reason == :not_open
|
|
310
|
+
redirect_to public_path(ctx[:job])
|
|
311
|
+
else
|
|
312
|
+
result.raise_policy_failed
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
See [Handling specific failure reasons inside an outcome block](#handling-specific-failure-reasons-inside-an-outcome-block)
|
|
318
|
+
for when `raise_policy_failed` and its siblings come in handy.
|
|
303
319
|
|
|
304
320
|
## Transactions
|
|
305
321
|
|
|
@@ -454,9 +470,50 @@ end
|
|
|
454
470
|
```
|
|
455
471
|
|
|
456
472
|
`failure(ctx, ...)` is a sequencer helper that builds a failed `Result`
|
|
457
|
-
with the sequencer's auto-derived i18n scope already applied. It takes
|
|
458
|
-
same
|
|
459
|
-
`
|
|
473
|
+
with the sequencer's auto-derived i18n scope already applied. It takes
|
|
474
|
+
the same kwargs as `Result.failure` (`code:`, `data:`, `step:`,
|
|
475
|
+
`i18n_scope:`, `i18n_key:`, `i18n_args:`).
|
|
476
|
+
|
|
477
|
+
## Outcome blocks and safety nets
|
|
478
|
+
|
|
479
|
+
`run_sequence` enforces that serious failures are addressed. Forgetting to
|
|
480
|
+
handle them raises rather than silently swallowing:
|
|
481
|
+
|
|
482
|
+
- An unhandled `:forbidden` raises `Hubbado::Sequence::Errors::Unauthorized`.
|
|
483
|
+
- An unhandled `:not_found` raises `Hubbado::Sequence::Errors::NotFound`.
|
|
484
|
+
- An unhandled non-policy / non-not-found failure (and an `otherwise` block
|
|
485
|
+
isn't given) raises `Hubbado::Sequence::Errors::Failed`.
|
|
486
|
+
|
|
487
|
+
`otherwise` deliberately does *not* catch `:forbidden` or `:not_found` —
|
|
488
|
+
that's what prevents a generic `otherwise` accidentally rendering a form
|
|
489
|
+
when the policy denied access.
|
|
490
|
+
|
|
491
|
+
### Handling specific failure reasons inside an outcome block
|
|
492
|
+
|
|
493
|
+
The dispatch object exposes the standard escalation paths as public
|
|
494
|
+
methods, so an outcome block can handle some cases inline and fall back to
|
|
495
|
+
the framework's exception for the rest:
|
|
496
|
+
|
|
497
|
+
```ruby
|
|
498
|
+
result.policy_failed do |ctx|
|
|
499
|
+
if result.data[:policy_result].reason == :not_open
|
|
500
|
+
redirect_to public_path(ctx[:job])
|
|
501
|
+
else
|
|
502
|
+
result.raise_policy_failed
|
|
503
|
+
end
|
|
504
|
+
end
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
The available helpers mirror the safety nets:
|
|
508
|
+
|
|
509
|
+
- `result.raise_policy_failed` — raises `Errors::Unauthorized` with the
|
|
510
|
+
standard message.
|
|
511
|
+
- `result.raise_not_found` — raises `Errors::NotFound`.
|
|
512
|
+
- `result.raise_failed` — raises `Errors::Failed`.
|
|
513
|
+
|
|
514
|
+
Use them when you genuinely want the framework's default escalation;
|
|
515
|
+
prefer plain Ruby control flow (`return`, `if`/`else`) for ordinary
|
|
516
|
+
branching.
|
|
460
517
|
|
|
461
518
|
## Testing
|
|
462
519
|
|
|
@@ -510,7 +567,7 @@ context "Seqs::UpdateUser::Present when the user is not found" do
|
|
|
510
567
|
result = seq.(params: { id: 999 }, current_user: User.new)
|
|
511
568
|
|
|
512
569
|
test "Fails with :not_found" do
|
|
513
|
-
assert(result.
|
|
570
|
+
assert(result.code == :not_found)
|
|
514
571
|
end
|
|
515
572
|
|
|
516
573
|
test "Does not build the contract" do
|
|
@@ -523,6 +580,15 @@ context "Seqs::UpdateUser::Present when the user is not found" do
|
|
|
523
580
|
end
|
|
524
581
|
```
|
|
525
582
|
|
|
583
|
+
The `Policy::Check` substitute's `fail_with(**error)` accepts the same
|
|
584
|
+
attributes `Result.failure` does, so a test that needs an outcome block
|
|
585
|
+
to branch on `result.data[:policy_result].reason` can configure the data
|
|
586
|
+
payload directly:
|
|
587
|
+
|
|
588
|
+
```ruby
|
|
589
|
+
seq.check_policy.fail_with(code: :forbidden, data: { policy_result: DeniedResult.new(:not_open) })
|
|
590
|
+
```
|
|
591
|
+
|
|
526
592
|
### Substituting a nested sequencer
|
|
527
593
|
|
|
528
594
|
Every sequencer ships a default `Substitute` module (installed by
|
|
@@ -567,7 +633,7 @@ context "Seqs::UpdateUser when Present denies access" do
|
|
|
567
633
|
end
|
|
568
634
|
|
|
569
635
|
test "Fails with :forbidden" do
|
|
570
|
-
assert(result.
|
|
636
|
+
assert(result.code == :forbidden)
|
|
571
637
|
end
|
|
572
638
|
|
|
573
639
|
test "Does not validate" do
|
|
@@ -586,7 +652,7 @@ context "Seqs::UpdateUser when Present cannot find the record" do
|
|
|
586
652
|
result = seq.(params: { id: 999, user: {} }, current_user: User.new)
|
|
587
653
|
|
|
588
654
|
test "Fails with :not_found" do
|
|
589
|
-
assert(result.
|
|
655
|
+
assert(result.code == :not_found)
|
|
590
656
|
end
|
|
591
657
|
|
|
592
658
|
test "Does not validate" do
|
|
@@ -610,12 +676,12 @@ to exercise Find / Build / Policy::Check directly — those live in
|
|
|
610
676
|
|
|
611
677
|
Every `Result` carries **successful_steps** — the list of step names that
|
|
612
678
|
completed successfully, in order. On failure, the failing step is *not* in
|
|
613
|
-
`successful_steps`; it's tagged on `
|
|
679
|
+
`successful_steps`; it's tagged on `step` instead.
|
|
614
680
|
|
|
615
681
|
```ruby
|
|
616
682
|
result.successful_steps # => [:find, :build_contract, :check_policy, :validate, :persist] # success
|
|
617
683
|
result.successful_steps # => [:find, :build_contract] # failed at :check_policy
|
|
618
|
-
result.
|
|
684
|
+
result.step # => :check_policy
|
|
619
685
|
```
|
|
620
686
|
|
|
621
687
|
When invoked via `run_sequence`, the dispatcher logs a single line per
|
data/hubbado-sequence.gemspec
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
|
2
2
|
Gem::Specification.new do |s|
|
|
3
3
|
s.name = "hubbado-sequence"
|
|
4
|
-
s.version = "0.
|
|
4
|
+
s.version = "0.6.0"
|
|
5
5
|
s.summary = "A small framework for the short sequences of common steps that controller actions usually boil down to"
|
|
6
6
|
s.description = "A sequencer takes input, runs an ordered sequence of steps, and returns a Result carrying a success-or-failure flag, a structured error, and the working context that was built up during execution. Built with Rails in mind but framework-agnostic."
|
|
7
7
|
|
|
@@ -31,7 +31,7 @@ module Hubbado
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
record def call(ctx, contract_class, attr_name = nil)
|
|
34
|
-
return Result.failure(ctx,
|
|
34
|
+
return Result.failure(ctx, **@configured_error) if @configured_error
|
|
35
35
|
|
|
36
36
|
ctx[:contract] = @return_value if @configured_success
|
|
37
37
|
Result.success(ctx)
|
|
@@ -16,7 +16,7 @@ module Hubbado
|
|
|
16
16
|
if contract.save
|
|
17
17
|
Result.success(ctx)
|
|
18
18
|
else
|
|
19
|
-
Result.failure(ctx,
|
|
19
|
+
Result.failure(ctx, code: :persist_failed)
|
|
20
20
|
end
|
|
21
21
|
end
|
|
22
22
|
|
|
@@ -34,7 +34,7 @@ module Hubbado
|
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
record def call(ctx)
|
|
37
|
-
return Result.failure(ctx,
|
|
37
|
+
return Result.failure(ctx, **@configured_error) if @configured_error
|
|
38
38
|
|
|
39
39
|
Result.success(ctx)
|
|
40
40
|
end
|
|
@@ -19,7 +19,7 @@ module Hubbado
|
|
|
19
19
|
if contract.errors.empty?
|
|
20
20
|
Result.success(ctx)
|
|
21
21
|
else
|
|
22
|
-
Result.failure(ctx,
|
|
22
|
+
Result.failure(ctx, code: :validation_failed)
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
25
|
|
|
@@ -37,7 +37,7 @@ module Hubbado
|
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
record def call(ctx, from: nil)
|
|
40
|
-
return Result.failure(ctx,
|
|
40
|
+
return Result.failure(ctx, **@configured_error) if @configured_error
|
|
41
41
|
|
|
42
42
|
Result.success(ctx)
|
|
43
43
|
end
|
|
@@ -40,7 +40,7 @@ module Hubbado
|
|
|
40
40
|
"Macros::Model::Build substitute: #{model} does not respond to :new"
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
return Result.failure(ctx,
|
|
43
|
+
return Result.failure(ctx, **@configured_error) if @configured_error
|
|
44
44
|
|
|
45
45
|
ctx[as] = @return_value if @configured_success
|
|
46
46
|
Result.success(ctx)
|
|
@@ -18,7 +18,7 @@ module Hubbado
|
|
|
18
18
|
ctx[as] = record
|
|
19
19
|
Result.success(ctx)
|
|
20
20
|
else
|
|
21
|
-
Result.failure(ctx,
|
|
21
|
+
Result.failure(ctx, code: :not_found)
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
24
|
|
|
@@ -42,7 +42,7 @@ module Hubbado
|
|
|
42
42
|
"Macros::Model::Find substitute: #{model} does not respond to :find_by"
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
-
return Result.failure(ctx,
|
|
45
|
+
return Result.failure(ctx, **@configured_error) if @configured_error
|
|
46
46
|
|
|
47
47
|
ctx[as] = @return_value if @configured_success
|
|
48
48
|
Result.success(ctx)
|
|
@@ -22,10 +22,8 @@ module Hubbado
|
|
|
22
22
|
else
|
|
23
23
|
Result.failure(
|
|
24
24
|
ctx,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
data: { policy: policy_instance, policy_result: policy_result }
|
|
28
|
-
}
|
|
25
|
+
code: :forbidden,
|
|
26
|
+
data: { policy: policy_instance, policy_result: policy_result }
|
|
29
27
|
)
|
|
30
28
|
end
|
|
31
29
|
end
|
|
@@ -49,7 +47,7 @@ module Hubbado
|
|
|
49
47
|
"Macros::Policy::Check substitute: #{policy} does not declare action :#{action}"
|
|
50
48
|
end
|
|
51
49
|
|
|
52
|
-
return Result.failure(ctx,
|
|
50
|
+
return Result.failure(ctx, **@configured_error) if @configured_error
|
|
53
51
|
|
|
54
52
|
Result.success(ctx)
|
|
55
53
|
end
|
|
@@ -84,16 +84,13 @@ module Hubbado
|
|
|
84
84
|
|
|
85
85
|
def record(name, return_value)
|
|
86
86
|
if return_value.is_a?(Result) && return_value.failure?
|
|
87
|
-
@failed_result =
|
|
87
|
+
@failed_result = return_value
|
|
88
|
+
.with_step(name)
|
|
89
|
+
.with_successful_steps(@successful_steps.dup)
|
|
88
90
|
else
|
|
89
91
|
@successful_steps << name
|
|
90
92
|
end
|
|
91
93
|
end
|
|
92
|
-
|
|
93
|
-
def tag_failure(result, step_name)
|
|
94
|
-
tagged_error = result.error.merge(step: step_name)
|
|
95
|
-
Result.failure(result.ctx, error: tagged_error, successful_steps: @successful_steps.dup, i18n_scope: result.i18n_scope)
|
|
96
|
-
end
|
|
97
94
|
end
|
|
98
95
|
end
|
|
99
96
|
end
|
|
@@ -4,28 +4,52 @@ module Hubbado
|
|
|
4
4
|
FRAMEWORK_I18N_SCOPE = "sequence.errors".freeze
|
|
5
5
|
|
|
6
6
|
attr_reader :ctx
|
|
7
|
-
attr_reader :
|
|
7
|
+
attr_reader :code
|
|
8
|
+
attr_reader :data
|
|
9
|
+
attr_reader :step
|
|
8
10
|
attr_reader :successful_steps
|
|
9
11
|
attr_reader :i18n_scope
|
|
12
|
+
attr_reader :i18n_key
|
|
13
|
+
attr_reader :i18n_args
|
|
10
14
|
|
|
11
15
|
def self.success(ctx, successful_steps: [], i18n_scope: nil)
|
|
12
|
-
new(
|
|
16
|
+
new(
|
|
17
|
+
:success,
|
|
18
|
+
ctx: ctx,
|
|
19
|
+
successful_steps: successful_steps,
|
|
20
|
+
i18n_scope: i18n_scope
|
|
21
|
+
)
|
|
13
22
|
end
|
|
14
23
|
|
|
15
|
-
def self.failure(ctx,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
def self.failure(ctx, code:, data: nil, step: nil,
|
|
25
|
+
i18n_scope: nil, i18n_key: nil, i18n_args: nil, successful_steps: [])
|
|
26
|
+
raise ArgumentError, "Result.failure requires code:" unless code
|
|
27
|
+
|
|
28
|
+
new(
|
|
29
|
+
:failure,
|
|
30
|
+
ctx: ctx,
|
|
31
|
+
code: code,
|
|
32
|
+
data: data,
|
|
33
|
+
step: step,
|
|
34
|
+
successful_steps: successful_steps,
|
|
35
|
+
i18n_scope: i18n_scope,
|
|
36
|
+
i18n_key: i18n_key,
|
|
37
|
+
i18n_args: i18n_args
|
|
38
|
+
)
|
|
21
39
|
end
|
|
22
40
|
|
|
23
|
-
def initialize(status, ctx
|
|
41
|
+
def initialize(status, ctx:, successful_steps:, i18n_scope:,
|
|
42
|
+
code: nil, data: nil, step: nil,
|
|
43
|
+
i18n_key: nil, i18n_args: nil)
|
|
24
44
|
@status = status
|
|
25
45
|
@ctx = ctx
|
|
26
|
-
@
|
|
46
|
+
@code = code
|
|
47
|
+
@data = data
|
|
48
|
+
@step = step
|
|
27
49
|
@successful_steps = successful_steps
|
|
28
50
|
@i18n_scope = i18n_scope
|
|
51
|
+
@i18n_key = i18n_key
|
|
52
|
+
@i18n_args = i18n_args
|
|
29
53
|
end
|
|
30
54
|
|
|
31
55
|
def success?
|
|
@@ -37,35 +61,50 @@ module Hubbado
|
|
|
37
61
|
end
|
|
38
62
|
|
|
39
63
|
def with_successful_steps(successful_steps)
|
|
40
|
-
|
|
64
|
+
copy(successful_steps: successful_steps)
|
|
41
65
|
end
|
|
42
66
|
|
|
43
67
|
def with_i18n_scope(scope)
|
|
44
68
|
return self unless @i18n_scope.nil?
|
|
45
69
|
|
|
46
|
-
|
|
70
|
+
copy(i18n_scope: scope)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def with_step(step)
|
|
74
|
+
copy(step: step)
|
|
47
75
|
end
|
|
48
76
|
|
|
49
77
|
def message
|
|
50
78
|
return nil if success?
|
|
51
79
|
|
|
52
|
-
|
|
53
|
-
return translation if translation
|
|
54
|
-
|
|
55
|
-
@error[:message] || humanize_code
|
|
80
|
+
translate_with_chain || humanize_code
|
|
56
81
|
end
|
|
57
82
|
|
|
58
83
|
private
|
|
59
84
|
|
|
85
|
+
def copy(**overrides)
|
|
86
|
+
self.class.new(
|
|
87
|
+
@status,
|
|
88
|
+
ctx: @ctx,
|
|
89
|
+
code: @code,
|
|
90
|
+
data: @data,
|
|
91
|
+
step: @step,
|
|
92
|
+
successful_steps: @successful_steps,
|
|
93
|
+
i18n_scope: @i18n_scope,
|
|
94
|
+
i18n_key: @i18n_key,
|
|
95
|
+
i18n_args: @i18n_args,
|
|
96
|
+
**overrides
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
|
|
60
100
|
def translate_with_chain
|
|
61
101
|
scopes = []
|
|
62
|
-
scopes << @error[:i18n_scope] if @error[:i18n_scope]
|
|
63
102
|
scopes << @i18n_scope if @i18n_scope
|
|
64
103
|
scopes << FRAMEWORK_I18N_SCOPE
|
|
65
104
|
scopes.uniq!
|
|
66
105
|
|
|
67
|
-
key = @
|
|
68
|
-
args = @
|
|
106
|
+
key = @i18n_key || @code
|
|
107
|
+
args = @i18n_args || {}
|
|
69
108
|
|
|
70
109
|
scopes.each do |scope|
|
|
71
110
|
translated = ::I18n.t("#{scope}.#{key}", default: nil, **args)
|
|
@@ -76,7 +115,7 @@ module Hubbado
|
|
|
76
115
|
end
|
|
77
116
|
|
|
78
117
|
def humanize_code
|
|
79
|
-
@
|
|
118
|
+
@code.to_s.tr("_", " ").capitalize
|
|
80
119
|
end
|
|
81
120
|
end
|
|
82
121
|
end
|
|
@@ -25,7 +25,7 @@ module Hubbado
|
|
|
25
25
|
class Dispatch
|
|
26
26
|
include Hubbado::Log::Dependency
|
|
27
27
|
|
|
28
|
-
attr_reader :returned, :
|
|
28
|
+
attr_reader :returned, :sequencer_class
|
|
29
29
|
|
|
30
30
|
def initialize(sequencer_class, result)
|
|
31
31
|
@sequencer_class = sequencer_class
|
|
@@ -33,6 +33,16 @@ module Hubbado
|
|
|
33
33
|
@handled = false
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
+
# Read-throughs to the wrapped Result. Outcome blocks read these on
|
|
37
|
+
# the Dispatch object (the block argument) without hopping through
|
|
38
|
+
# an inner Result reference.
|
|
39
|
+
def code = @result.code
|
|
40
|
+
def data = @result.data
|
|
41
|
+
def step = @result.step
|
|
42
|
+
def message = @result.message
|
|
43
|
+
def successful_steps = @result.successful_steps
|
|
44
|
+
def ctx = @result.ctx
|
|
45
|
+
|
|
36
46
|
def success
|
|
37
47
|
return unless @result.success?
|
|
38
48
|
execute { yield(@result.ctx) }
|
|
@@ -69,29 +79,38 @@ module Hubbado
|
|
|
69
79
|
logger.info("Sequencer #{@sequencer_class.name} failed at #{step_label} (#{code}): #{steps_summary}")
|
|
70
80
|
end
|
|
71
81
|
|
|
72
|
-
def code
|
|
73
|
-
@result.error&.[](:code)
|
|
74
|
-
end
|
|
75
|
-
|
|
76
82
|
def handled?
|
|
77
83
|
@result.success? || @handled
|
|
78
84
|
end
|
|
79
85
|
|
|
86
|
+
# Raise the standard policy-denial exception. Available inside an
|
|
87
|
+
# outcome block (e.g. for callers that handle some policy reasons
|
|
88
|
+
# inline and want the framework's standard escalation for the rest)
|
|
89
|
+
# and used internally by enforce_safety_nets! when no handler ran.
|
|
90
|
+
def raise_policy_failed
|
|
91
|
+
raise Errors::Unauthorized.new(
|
|
92
|
+
"#{@sequencer_class.name} denied: #{@result.message}",
|
|
93
|
+
@result
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def raise_not_found
|
|
98
|
+
raise Errors::NotFound, "#{@sequencer_class.name} reported not_found"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def raise_failed
|
|
102
|
+
raise Errors::Failed, "#{@sequencer_class.name} failed (#{code}): #{@result.message}"
|
|
103
|
+
end
|
|
104
|
+
|
|
80
105
|
def enforce_safety_nets!
|
|
81
106
|
return if handled?
|
|
82
107
|
|
|
83
108
|
log_unhandled
|
|
84
109
|
|
|
85
110
|
case code
|
|
86
|
-
when :forbidden
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
@result
|
|
90
|
-
)
|
|
91
|
-
when :not_found
|
|
92
|
-
raise Errors::NotFound, "#{@sequencer_class.name} reported not_found"
|
|
93
|
-
else
|
|
94
|
-
raise Errors::Failed, "#{@sequencer_class.name} failed (#{code}): #{@result.message}"
|
|
111
|
+
when :forbidden then raise_policy_failed
|
|
112
|
+
when :not_found then raise_not_found
|
|
113
|
+
else raise_failed
|
|
95
114
|
end
|
|
96
115
|
end
|
|
97
116
|
|
|
@@ -107,11 +126,10 @@ module Hubbado
|
|
|
107
126
|
end
|
|
108
127
|
|
|
109
128
|
def steps_summary
|
|
110
|
-
|
|
129
|
+
successful_steps.empty? ? "(no steps)" : successful_steps.map(&:to_s).join(" → ")
|
|
111
130
|
end
|
|
112
131
|
|
|
113
132
|
def step_label
|
|
114
|
-
step = @result.error && @result.error[:step]
|
|
115
133
|
step ? step.inspect : "(unknown step)"
|
|
116
134
|
end
|
|
117
135
|
end
|
|
@@ -162,7 +180,8 @@ module Hubbado
|
|
|
162
180
|
def configure_failure(code, error_attrs)
|
|
163
181
|
@configured_outcome = {
|
|
164
182
|
kind: :failure,
|
|
165
|
-
|
|
183
|
+
code: code,
|
|
184
|
+
error_attrs: error_attrs
|
|
166
185
|
}
|
|
167
186
|
self
|
|
168
187
|
end
|
|
@@ -175,7 +194,7 @@ module Hubbado
|
|
|
175
194
|
outcome[:ctx_writes].each { |key, value| ctx[key] = value }
|
|
176
195
|
Hubbado::Sequence::Result.success(ctx)
|
|
177
196
|
else
|
|
178
|
-
Hubbado::Sequence::Result.failure(ctx,
|
|
197
|
+
Hubbado::Sequence::Result.failure(ctx, code: outcome[:code], **outcome[:error_attrs])
|
|
179
198
|
end
|
|
180
199
|
end
|
|
181
200
|
end
|
|
@@ -34,7 +34,7 @@ module Hubbado
|
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
record def call(ctx)
|
|
37
|
-
return ::Hubbado::Sequence::Result.failure(ctx,
|
|
37
|
+
return ::Hubbado::Sequence::Result.failure(ctx, **@configured_error) if @configured_error
|
|
38
38
|
|
|
39
39
|
if @configured_writes
|
|
40
40
|
@configured_writes.each { |k, v| ctx[k] = v }
|
|
@@ -83,7 +83,8 @@ module Hubbado
|
|
|
83
83
|
end
|
|
84
84
|
|
|
85
85
|
def failure(ctx, **error_attrs)
|
|
86
|
-
|
|
86
|
+
error_attrs[:i18n_scope] ||= i18n_scope
|
|
87
|
+
Result.failure(ctx, **error_attrs)
|
|
87
88
|
end
|
|
88
89
|
|
|
89
90
|
# Builds a Pipeline that auto-dispatches blockless `step(:foo)` calls to
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hubbado-sequence
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Hubbado Devs
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-05-
|
|
11
|
+
date: 2026-05-16 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: evt-casing
|