hubbado-sequence 0.4.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 +139 -0
- data/README.md +315 -162
- data/hubbado-sequence.gemspec +3 -3
- data/lib/hubbado/sequence/macros/contract/build.rb +3 -3
- data/lib/hubbado/sequence/macros/contract/deserialize.rb +3 -3
- data/lib/hubbado/sequence/macros/contract/persist.rb +4 -4
- data/lib/hubbado/sequence/macros/contract/validate.rb +4 -4
- data/lib/hubbado/sequence/macros/model/build.rb +3 -3
- data/lib/hubbado/sequence/macros/model/find.rb +4 -4
- data/lib/hubbado/sequence/macros/policy/check.rb +6 -8
- data/lib/hubbado/sequence/pipeline.rb +9 -12
- data/lib/hubbado/sequence/result.rb +68 -29
- data/lib/hubbado/sequence/runner.rb +47 -28
- data/lib/hubbado/sequence/sequencer.rb +4 -3
- metadata +7 -6
data/hubbado-sequence.gemspec
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
|
2
2
|
Gem::Specification.new do |s|
|
|
3
3
|
s.name = "hubbado-sequence"
|
|
4
|
-
s.version = "0.
|
|
5
|
-
s.summary = "A small framework for
|
|
6
|
-
s.description = "
|
|
4
|
+
s.version = "0.6.0"
|
|
5
|
+
s.summary = "A small framework for the short sequences of common steps that controller actions usually boil down to"
|
|
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
|
|
|
8
8
|
s.authors = ["Hubbado Devs"]
|
|
9
9
|
s.email = ["devs@hubbado.com"]
|
|
@@ -13,7 +13,7 @@ module Hubbado
|
|
|
13
13
|
def call(ctx, contract_class, attr_name = nil)
|
|
14
14
|
model = attr_name && Path.resolve(ctx, attr_name)
|
|
15
15
|
ctx[:contract] = contract_class.new(model)
|
|
16
|
-
Result.
|
|
16
|
+
Result.success(ctx)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
module Substitute
|
|
@@ -31,10 +31,10 @@ module Hubbado
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
record def call(ctx, contract_class, attr_name = nil)
|
|
34
|
-
return Result.
|
|
34
|
+
return Result.failure(ctx, **@configured_error) if @configured_error
|
|
35
35
|
|
|
36
36
|
ctx[:contract] = @return_value if @configured_success
|
|
37
|
-
Result.
|
|
37
|
+
Result.success(ctx)
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
def built?(**kwargs)
|
|
@@ -15,7 +15,7 @@ module Hubbado
|
|
|
15
15
|
|
|
16
16
|
ctx[:contract].deserialize(params) if params
|
|
17
17
|
|
|
18
|
-
Result.
|
|
18
|
+
Result.success(ctx)
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
module Substitute
|
|
@@ -27,9 +27,9 @@ module Hubbado
|
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
record def call(ctx, from:)
|
|
30
|
-
return Result.
|
|
30
|
+
return Result.failure(ctx, **@configured_error) if @configured_error
|
|
31
31
|
|
|
32
|
-
Result.
|
|
32
|
+
Result.success(ctx)
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
def deserialized?(**kwargs)
|
|
@@ -14,9 +14,9 @@ module Hubbado
|
|
|
14
14
|
contract = ctx[:contract]
|
|
15
15
|
|
|
16
16
|
if contract.save
|
|
17
|
-
Result.
|
|
17
|
+
Result.success(ctx)
|
|
18
18
|
else
|
|
19
|
-
Result.
|
|
19
|
+
Result.failure(ctx, code: :persist_failed)
|
|
20
20
|
end
|
|
21
21
|
end
|
|
22
22
|
|
|
@@ -34,9 +34,9 @@ module Hubbado
|
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
record def call(ctx)
|
|
37
|
-
return Result.
|
|
37
|
+
return Result.failure(ctx, **@configured_error) if @configured_error
|
|
38
38
|
|
|
39
|
-
Result.
|
|
39
|
+
Result.success(ctx)
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
def persisted?(**kwargs)
|
|
@@ -17,9 +17,9 @@ module Hubbado
|
|
|
17
17
|
contract.validate(params)
|
|
18
18
|
|
|
19
19
|
if contract.errors.empty?
|
|
20
|
-
Result.
|
|
20
|
+
Result.success(ctx)
|
|
21
21
|
else
|
|
22
|
-
Result.
|
|
22
|
+
Result.failure(ctx, code: :validation_failed)
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
25
|
|
|
@@ -37,9 +37,9 @@ module Hubbado
|
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
record def call(ctx, from: nil)
|
|
40
|
-
return Result.
|
|
40
|
+
return Result.failure(ctx, **@configured_error) if @configured_error
|
|
41
41
|
|
|
42
|
-
Result.
|
|
42
|
+
Result.success(ctx)
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
def validated?(**kwargs)
|
|
@@ -17,7 +17,7 @@ module Hubbado
|
|
|
17
17
|
else
|
|
18
18
|
model.new(attributes)
|
|
19
19
|
end
|
|
20
|
-
Result.
|
|
20
|
+
Result.success(ctx)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
module Substitute
|
|
@@ -40,10 +40,10 @@ module Hubbado
|
|
|
40
40
|
"Macros::Model::Build substitute: #{model} does not respond to :new"
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
return Result.
|
|
43
|
+
return Result.failure(ctx, **@configured_error) if @configured_error
|
|
44
44
|
|
|
45
45
|
ctx[as] = @return_value if @configured_success
|
|
46
|
-
Result.
|
|
46
|
+
Result.success(ctx)
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
def built?(**kwargs)
|
|
@@ -16,9 +16,9 @@ module Hubbado
|
|
|
16
16
|
|
|
17
17
|
if record
|
|
18
18
|
ctx[as] = record
|
|
19
|
-
Result.
|
|
19
|
+
Result.success(ctx)
|
|
20
20
|
else
|
|
21
|
-
Result.
|
|
21
|
+
Result.failure(ctx, code: :not_found)
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
24
|
|
|
@@ -42,10 +42,10 @@ module Hubbado
|
|
|
42
42
|
"Macros::Model::Find substitute: #{model} does not respond to :find_by"
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
-
return Result.
|
|
45
|
+
return Result.failure(ctx, **@configured_error) if @configured_error
|
|
46
46
|
|
|
47
47
|
ctx[as] = @return_value if @configured_success
|
|
48
|
-
Result.
|
|
48
|
+
Result.success(ctx)
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
def fetched?(**kwargs)
|
|
@@ -18,14 +18,12 @@ module Hubbado
|
|
|
18
18
|
policy_result = policy_instance.public_send(action)
|
|
19
19
|
|
|
20
20
|
if policy_result.permitted?
|
|
21
|
-
Result.
|
|
21
|
+
Result.success(ctx)
|
|
22
22
|
else
|
|
23
|
-
Result.
|
|
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,9 +47,9 @@ module Hubbado
|
|
|
49
47
|
"Macros::Policy::Check substitute: #{policy} does not declare action :#{action}"
|
|
50
48
|
end
|
|
51
49
|
|
|
52
|
-
return Result.
|
|
50
|
+
return Result.failure(ctx, **@configured_error) if @configured_error
|
|
53
51
|
|
|
54
|
-
Result.
|
|
52
|
+
Result.success(ctx)
|
|
55
53
|
end
|
|
56
54
|
|
|
57
55
|
def checked?(**kwargs)
|
|
@@ -6,15 +6,15 @@ module Hubbado
|
|
|
6
6
|
class Pipeline
|
|
7
7
|
def initialize(ctx, dispatcher:)
|
|
8
8
|
@ctx = ctx
|
|
9
|
-
@
|
|
9
|
+
@successful_steps = []
|
|
10
10
|
@failed_result = nil
|
|
11
11
|
@dispatcher = dispatcher
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
# `step(:name)` dispatches to `dispatcher.send(name, ctx)`. The method
|
|
15
15
|
# is treated as successful unless it explicitly returns a failed
|
|
16
|
-
# `Result`; any other return value (nil, false, a model, `Result.
|
|
17
|
-
# continues the pipeline with the same ctx. Only `Result.
|
|
16
|
+
# `Result`; any other return value (nil, false, a model, `Result.success`)
|
|
17
|
+
# continues the pipeline with the same ctx. Only `Result.failure(...)` /
|
|
18
18
|
# `failure(ctx, code: ...)` short-circuits.
|
|
19
19
|
def step(name)
|
|
20
20
|
return self if @failed_result
|
|
@@ -25,7 +25,7 @@ module Hubbado
|
|
|
25
25
|
|
|
26
26
|
# `invoke(:name, *args, **kwargs)` calls a declared dependency on the
|
|
27
27
|
# dispatcher: gets it via `dispatcher.send(name)` (the reader), then
|
|
28
|
-
# invokes it with `(ctx, *args, **kwargs)`. Same
|
|
28
|
+
# invokes it with `(ctx, *args, **kwargs)`. Same step recording,
|
|
29
29
|
# failure short-circuiting, and lenient return convention as `step`.
|
|
30
30
|
#
|
|
31
31
|
# Use this for any declared dependency — macros
|
|
@@ -58,7 +58,7 @@ module Hubbado
|
|
|
58
58
|
if @failed_result
|
|
59
59
|
@failed_result
|
|
60
60
|
else
|
|
61
|
-
Result.
|
|
61
|
+
Result.success(@ctx, successful_steps: @successful_steps.dup)
|
|
62
62
|
end
|
|
63
63
|
end
|
|
64
64
|
|
|
@@ -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.fail(result.ctx, error: tagged_error, trail: @trail.dup, i18n_scope: result.i18n_scope)
|
|
96
|
-
end
|
|
97
94
|
end
|
|
98
95
|
end
|
|
99
96
|
end
|
|
@@ -4,68 +4,107 @@ module Hubbado
|
|
|
4
4
|
FRAMEWORK_I18N_SCOPE = "sequence.errors".freeze
|
|
5
5
|
|
|
6
6
|
attr_reader :ctx
|
|
7
|
-
attr_reader :
|
|
8
|
-
attr_reader :
|
|
7
|
+
attr_reader :code
|
|
8
|
+
attr_reader :data
|
|
9
|
+
attr_reader :step
|
|
10
|
+
attr_reader :successful_steps
|
|
9
11
|
attr_reader :i18n_scope
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
attr_reader :i18n_key
|
|
13
|
+
attr_reader :i18n_args
|
|
14
|
+
|
|
15
|
+
def self.success(ctx, successful_steps: [], i18n_scope: nil)
|
|
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.
|
|
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
|
-
@
|
|
27
|
-
@
|
|
46
|
+
@code = code
|
|
47
|
+
@data = data
|
|
48
|
+
@step = step
|
|
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
|
-
def
|
|
32
|
-
@status == :
|
|
55
|
+
def success?
|
|
56
|
+
@status == :success
|
|
33
57
|
end
|
|
34
58
|
|
|
35
59
|
def failure?
|
|
36
|
-
@status == :
|
|
60
|
+
@status == :failure
|
|
37
61
|
end
|
|
38
62
|
|
|
39
|
-
def
|
|
40
|
-
|
|
63
|
+
def with_successful_steps(successful_steps)
|
|
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)
|
|
47
71
|
end
|
|
48
72
|
|
|
49
|
-
def
|
|
50
|
-
|
|
73
|
+
def with_step(step)
|
|
74
|
+
copy(step: step)
|
|
75
|
+
end
|
|
51
76
|
|
|
52
|
-
|
|
53
|
-
return
|
|
77
|
+
def message
|
|
78
|
+
return nil if success?
|
|
54
79
|
|
|
55
|
-
|
|
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,48 +33,73 @@ 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
|
-
return unless @result.
|
|
47
|
+
return unless @result.success?
|
|
38
48
|
execute { yield(@result.ctx) }
|
|
39
|
-
logger.info("Sequencer #{@sequencer_class.name} succeeded: #{
|
|
49
|
+
logger.info("Sequencer #{@sequencer_class.name} succeeded: #{steps_summary}")
|
|
40
50
|
end
|
|
41
51
|
|
|
42
52
|
def policy_failed
|
|
43
53
|
return unless code == :forbidden
|
|
44
54
|
execute { yield(@result.ctx) }
|
|
45
|
-
logger.info("Sequencer #{@sequencer_class.name} policy failed at #{step_label} (#{code}): #{
|
|
55
|
+
logger.info("Sequencer #{@sequencer_class.name} policy failed at #{step_label} (#{code}): #{steps_summary}")
|
|
46
56
|
end
|
|
47
57
|
|
|
48
58
|
def not_found
|
|
49
59
|
return unless code == :not_found
|
|
50
60
|
execute { yield(@result.ctx) }
|
|
51
|
-
logger.info("Sequencer #{@sequencer_class.name} not found at #{step_label}: #{
|
|
61
|
+
logger.info("Sequencer #{@sequencer_class.name} not found at #{step_label}: #{steps_summary}")
|
|
52
62
|
end
|
|
53
63
|
|
|
54
64
|
def validation_failed
|
|
55
65
|
return unless code == :validation_failed
|
|
56
66
|
execute { yield(@result.ctx) }
|
|
57
|
-
logger.info("Sequencer #{@sequencer_class.name} validation failed at #{step_label}: #{
|
|
67
|
+
logger.info("Sequencer #{@sequencer_class.name} validation failed at #{step_label}: #{steps_summary}")
|
|
58
68
|
end
|
|
59
69
|
|
|
60
70
|
# otherwise deliberately does not catch policy denials or not_found —
|
|
61
71
|
# those have their own required handlers.
|
|
62
72
|
def otherwise
|
|
63
|
-
return if @result.
|
|
73
|
+
return if @result.success?
|
|
64
74
|
return if code == :forbidden
|
|
65
75
|
return if code == :not_found
|
|
66
76
|
return if @handled
|
|
67
77
|
|
|
68
78
|
execute { yield(@result.ctx) }
|
|
69
|
-
logger.info("Sequencer #{@sequencer_class.name} failed at #{step_label} (#{code}): #{
|
|
79
|
+
logger.info("Sequencer #{@sequencer_class.name} failed at #{step_label} (#{code}): #{steps_summary}")
|
|
70
80
|
end
|
|
71
81
|
|
|
72
|
-
def
|
|
73
|
-
@result.
|
|
82
|
+
def handled?
|
|
83
|
+
@result.success? || @handled
|
|
74
84
|
end
|
|
75
85
|
|
|
76
|
-
|
|
77
|
-
|
|
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}"
|
|
78
103
|
end
|
|
79
104
|
|
|
80
105
|
def enforce_safety_nets!
|
|
@@ -83,20 +108,14 @@ module Hubbado
|
|
|
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
|
|
|
98
117
|
def log_unhandled
|
|
99
|
-
logger.error("Sequencer #{@sequencer_class.name} failed unhandled at #{step_label} (#{code}): #{
|
|
118
|
+
logger.error("Sequencer #{@sequencer_class.name} failed unhandled at #{step_label} (#{code}): #{steps_summary}")
|
|
100
119
|
end
|
|
101
120
|
|
|
102
121
|
private
|
|
@@ -106,12 +125,11 @@ module Hubbado
|
|
|
106
125
|
@returned = yield
|
|
107
126
|
end
|
|
108
127
|
|
|
109
|
-
def
|
|
110
|
-
|
|
128
|
+
def steps_summary
|
|
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
|
|
@@ -173,9 +192,9 @@ module Hubbado
|
|
|
173
192
|
|
|
174
193
|
if outcome[:kind] == :success
|
|
175
194
|
outcome[:ctx_writes].each { |key, value| ctx[key] = value }
|
|
176
|
-
Hubbado::Sequence::Result.
|
|
195
|
+
Hubbado::Sequence::Result.success(ctx)
|
|
177
196
|
else
|
|
178
|
-
Hubbado::Sequence::Result.
|
|
197
|
+
Hubbado::Sequence::Result.failure(ctx, code: outcome[:code], **outcome[:error_attrs])
|
|
179
198
|
end
|
|
180
199
|
end
|
|
181
200
|
end
|
|
@@ -34,12 +34,12 @@ module Hubbado
|
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
record def call(ctx)
|
|
37
|
-
return ::Hubbado::Sequence::Result.
|
|
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 }
|
|
41
41
|
end
|
|
42
|
-
::Hubbado::Sequence::Result.
|
|
42
|
+
::Hubbado::Sequence::Result.success(ctx)
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
def called?(**kwargs)
|
|
@@ -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
|
|
@@ -164,9 +164,9 @@ dependencies:
|
|
|
164
164
|
- - ">="
|
|
165
165
|
- !ruby/object:Gem::Version
|
|
166
166
|
version: '0'
|
|
167
|
-
description:
|
|
168
|
-
|
|
169
|
-
execution.
|
|
167
|
+
description: A sequencer takes input, runs an ordered sequence of steps, and returns
|
|
168
|
+
a Result carrying a success-or-failure flag, a structured error, and the working
|
|
169
|
+
context that was built up during execution. Built with Rails in mind but framework-agnostic.
|
|
170
170
|
email:
|
|
171
171
|
- devs@hubbado.com
|
|
172
172
|
executables: []
|
|
@@ -223,5 +223,6 @@ requirements: []
|
|
|
223
223
|
rubygems_version: 3.5.22
|
|
224
224
|
signing_key:
|
|
225
225
|
specification_version: 4
|
|
226
|
-
summary: A small framework for
|
|
226
|
+
summary: A small framework for the short sequences of common steps that controller
|
|
227
|
+
actions usually boil down to
|
|
227
228
|
test_files: []
|