hubbado-sequence 0.3.0 → 0.4.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 +34 -0
- data/README.md +18 -14
- data/hubbado-sequence.gemspec +1 -1
- data/lib/hubbado/sequence/pipeline.rb +34 -76
- 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: 225c967e73e28a28effbc26e9c3bbdfce3357932a8374046dc46056d0dd8dd7d
|
|
4
|
+
data.tar.gz: 7a446faedf4bb88018c17a6130f29ec85f1fc354929864aaa2dd55f86753ffca
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f5ae7e6141a9d45576b0b93a0c25ac23a63a9d3a5b4ea5ba134103a91db20633a17a1dc4e41a7a73570dc6745446d55e4e22612249dd75211d9c2be1c02a3143
|
|
7
|
+
data.tar.gz: a741030d0046941b128a18bc8bb0f58f10bdb677cd8941857d8937263e4dd2c58aaaac6fd2c816deac3f8d310a256c67af516dc7e8428aa3e5e29791cafa3676
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,40 @@ 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.4.0] - Inline step blocks removed; Pipeline made internal
|
|
8
|
+
|
|
9
|
+
### Removed (breaking)
|
|
10
|
+
|
|
11
|
+
- **Inline block form of `step` removed.** `p.step(:name) { |ctx| ... }`
|
|
12
|
+
is no longer supported. Every step must be a method on the sequencer
|
|
13
|
+
with the same name:
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
# before
|
|
17
|
+
p.step(:notify) { |ctx| UserMailer.updated(ctx[:user]).deliver_later }
|
|
18
|
+
|
|
19
|
+
# after
|
|
20
|
+
p.step(:notify) # dispatches to def notify(ctx)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Migration: extract each inline block to a private method of the same
|
|
24
|
+
name. One method per step; one method per responsibility.
|
|
25
|
+
|
|
26
|
+
- **`Pipeline` is no longer part of the public API.** `Pipeline.(ctx)`
|
|
27
|
+
and `Pipeline.new` are internal to the framework. Sequencers build
|
|
28
|
+
pipelines exclusively through the `pipeline(ctx)` helper.
|
|
29
|
+
|
|
30
|
+
Migration: any direct `Pipeline.(ctx)` or `Pipeline.new(ctx, ...)`
|
|
31
|
+
call sites must be replaced with a sequencer that uses `pipeline(ctx)`.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- **`Pipeline#step` always auto-dispatches.** With inline blocks gone,
|
|
36
|
+
`step(:foo)` unconditionally dispatches to `self.foo(ctx)` on the
|
|
37
|
+
sequencer — no block-versus-dispatch ambiguity. Missing methods raise
|
|
38
|
+
`NoMethodError` with the step name and the sequencer class in the
|
|
39
|
+
message.
|
|
40
|
+
|
|
7
41
|
## [0.3.0] - Contract::Deserialize macro, Runner extraction, Path helper
|
|
8
42
|
|
|
9
43
|
### Added
|
data/README.md
CHANGED
|
@@ -114,29 +114,27 @@ class UsersController < ApplicationController
|
|
|
114
114
|
end
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
-
## The
|
|
117
|
+
## The two step shapes
|
|
118
118
|
|
|
119
119
|
```ruby
|
|
120
120
|
pipeline(ctx) do |p|
|
|
121
|
-
p.invoke(:find, :user)
|
|
122
|
-
p.step(:scrub_params)
|
|
123
|
-
p.step(:audit) { |c| AuditLog.append(c[:user]) } # inline block
|
|
121
|
+
p.invoke(:find, User, as: :user) # declared dependency (macro or sequencer)
|
|
122
|
+
p.step(:scrub_params) # local method `def scrub_params(ctx)`
|
|
124
123
|
end
|
|
125
124
|
```
|
|
126
125
|
|
|
127
126
|
- `p.invoke(:foo, *args, **kwargs)` — a `dependency :foo, …` declared on the
|
|
128
127
|
sequencer (a macro or a nested sequencer). Calls
|
|
129
128
|
`dispatcher.foo.(ctx, *args, **kwargs)`.
|
|
130
|
-
- `p.step(:foo)` — a local instance method.
|
|
131
|
-
`self.foo(ctx)`.
|
|
132
|
-
- `p.step(:foo) { |ctx| … }` — explicit inline block.
|
|
129
|
+
- `p.step(:foo)` — a local instance method. Dispatches to `self.foo(ctx)`.
|
|
133
130
|
|
|
134
|
-
|
|
135
|
-
`
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
131
|
+
Every `step` is a method on the sequencer with the same name as the step.
|
|
132
|
+
This makes the `call` body a table of contents — scan `p.step(:...)` lines
|
|
133
|
+
to see the sequence shape, jump to the method for details.
|
|
134
|
+
|
|
135
|
+
`pipeline(ctx)` is the only way to build a pipeline. The underlying
|
|
136
|
+
Pipeline class is an implementation detail; sequencers do not construct
|
|
137
|
+
it directly.
|
|
140
138
|
|
|
141
139
|
## Built-in macros
|
|
142
140
|
|
|
@@ -273,9 +271,15 @@ def call(ctx)
|
|
|
273
271
|
t.invoke(:persist)
|
|
274
272
|
end
|
|
275
273
|
|
|
276
|
-
p.step(:notify)
|
|
274
|
+
p.step(:notify)
|
|
277
275
|
end
|
|
278
276
|
end
|
|
277
|
+
|
|
278
|
+
private
|
|
279
|
+
|
|
280
|
+
def notify(ctx)
|
|
281
|
+
UserMailer.updated(ctx[:user]).deliver_later
|
|
282
|
+
end
|
|
279
283
|
```
|
|
280
284
|
|
|
281
285
|
Steps before the transaction run outside it (read-only lookups, policy
|
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.4.0"
|
|
5
5
|
s.summary = "A small framework for orchestrating units of business behaviour"
|
|
6
6
|
s.description = "Sequencer takes input, runs a sequence of steps, and returns a Result indicating success or failure plus the working context that was built up during execution."
|
|
7
7
|
|
|
@@ -1,84 +1,45 @@
|
|
|
1
1
|
module Hubbado
|
|
2
2
|
module Sequence
|
|
3
|
+
# Railway-style step runner that backs the Sequencer mixin's
|
|
4
|
+
# `pipeline(ctx)` helper. Not part of the public API — sequencers reach
|
|
5
|
+
# it through the helper.
|
|
3
6
|
class Pipeline
|
|
4
|
-
|
|
5
|
-
# runs the block (so steps can be added in statement form), and returns
|
|
6
|
-
# the final Result. The non-block form returns the Pipeline so chained
|
|
7
|
-
# `.step(...)...result` calls still work.
|
|
8
|
-
def self.call(ctx = nil, **kwargs, &block)
|
|
9
|
-
if ctx.nil?
|
|
10
|
-
ctx = Ctx.build(kwargs)
|
|
11
|
-
elsif !kwargs.empty?
|
|
12
|
-
raise ArgumentError, "Pipeline.() takes either a Ctx or keyword arguments, not both"
|
|
13
|
-
elsif !ctx.is_a?(Ctx)
|
|
14
|
-
ctx = Ctx.build(ctx)
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
pipe = new(ctx)
|
|
18
|
-
|
|
19
|
-
if block
|
|
20
|
-
block.call(pipe)
|
|
21
|
-
pipe.result
|
|
22
|
-
else
|
|
23
|
-
pipe
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def initialize(ctx, dispatcher: nil)
|
|
7
|
+
def initialize(ctx, dispatcher:)
|
|
28
8
|
@ctx = ctx
|
|
29
9
|
@trail = []
|
|
30
10
|
@failed_result = nil
|
|
31
11
|
@dispatcher = dispatcher
|
|
32
12
|
end
|
|
33
13
|
|
|
34
|
-
# `step(:name)
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
|
|
40
|
-
# explicitly returns a failed `Result`. Any other return value (nil,
|
|
41
|
-
# false, a model, a hash, `Result.ok(...)`) is taken as success and the
|
|
42
|
-
# pipeline continues with the same `@ctx`. Only `Result.fail(...)` /
|
|
43
|
-
# `failure(ctx, code: ...)` short-circuits the pipeline.
|
|
44
|
-
def step(name, &block)
|
|
14
|
+
# `step(:name)` dispatches to `dispatcher.send(name, ctx)`. The method
|
|
15
|
+
# is treated as successful unless it explicitly returns a failed
|
|
16
|
+
# `Result`; any other return value (nil, false, a model, `Result.ok`)
|
|
17
|
+
# continues the pipeline with the same ctx. Only `Result.fail(...)` /
|
|
18
|
+
# `failure(ctx, code: ...)` short-circuits.
|
|
19
|
+
def step(name)
|
|
45
20
|
return self if @failed_result
|
|
46
21
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if return_value.is_a?(Result) && return_value.failure?
|
|
50
|
-
@failed_result = tag_failure(return_value, name)
|
|
51
|
-
else
|
|
52
|
-
@trail << name
|
|
53
|
-
end
|
|
54
|
-
|
|
22
|
+
record(name, invoke_step(name))
|
|
55
23
|
self
|
|
56
24
|
end
|
|
57
25
|
|
|
58
26
|
# `invoke(:name, *args, **kwargs)` calls a declared dependency on the
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
# `step`.
|
|
27
|
+
# dispatcher: gets it via `dispatcher.send(name)` (the reader), then
|
|
28
|
+
# invokes it with `(ctx, *args, **kwargs)`. Same trail recording,
|
|
29
|
+
# failure short-circuiting, and lenient return convention as `step`.
|
|
63
30
|
#
|
|
64
|
-
# Use this for any declared dependency — macros
|
|
65
|
-
# and nested sequencers (`Seqs::Present`)
|
|
66
|
-
# instance methods like
|
|
31
|
+
# Use this for any declared dependency — macros
|
|
32
|
+
# (`Macros::Model::Find`) and nested sequencers (`Seqs::Present`)
|
|
33
|
+
# alike. Use `step` for local instance methods like
|
|
34
|
+
# `def deserialize_contract(ctx)`.
|
|
67
35
|
def invoke(name, *args, **kwargs)
|
|
68
36
|
return self if @failed_result
|
|
69
37
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if return_value.is_a?(Result) && return_value.failure?
|
|
73
|
-
@failed_result = tag_failure(return_value, name)
|
|
74
|
-
else
|
|
75
|
-
@trail << name
|
|
76
|
-
end
|
|
77
|
-
|
|
38
|
+
record(name, invoke_dependency(name, args, kwargs))
|
|
78
39
|
self
|
|
79
40
|
end
|
|
80
41
|
|
|
81
|
-
def transaction
|
|
42
|
+
def transaction
|
|
82
43
|
return self if @failed_result
|
|
83
44
|
|
|
84
45
|
if defined?(::ActiveRecord::Base)
|
|
@@ -103,27 +64,16 @@ module Hubbado
|
|
|
103
64
|
|
|
104
65
|
private
|
|
105
66
|
|
|
106
|
-
def invoke_step(name
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
unless @dispatcher.respond_to?(name, true)
|
|
111
|
-
raise NoMethodError,
|
|
112
|
-
"Pipeline step :#{name} expects #{@dispatcher.class.name} to define ##{name}, but it does not"
|
|
113
|
-
end
|
|
114
|
-
@dispatcher.send(name, @ctx)
|
|
115
|
-
else
|
|
116
|
-
raise ArgumentError,
|
|
117
|
-
"Pipeline step :#{name} needs either a block or a dispatcher (use the sequencer's `pipeline(ctx)` helper to enable auto-dispatch)"
|
|
67
|
+
def invoke_step(name)
|
|
68
|
+
unless @dispatcher.respond_to?(name, true)
|
|
69
|
+
raise NoMethodError,
|
|
70
|
+
"Pipeline step :#{name} expects #{@dispatcher.class.name} to define ##{name}, but it does not"
|
|
118
71
|
end
|
|
72
|
+
|
|
73
|
+
@dispatcher.send(name, @ctx)
|
|
119
74
|
end
|
|
120
75
|
|
|
121
76
|
def invoke_dependency(name, args, kwargs)
|
|
122
|
-
unless @dispatcher
|
|
123
|
-
raise ArgumentError,
|
|
124
|
-
"Pipeline#invoke :#{name} requires a dispatcher (use the sequencer's `pipeline(ctx)` helper)"
|
|
125
|
-
end
|
|
126
|
-
|
|
127
77
|
unless @dispatcher.respond_to?(name, true)
|
|
128
78
|
raise NoMethodError,
|
|
129
79
|
"Pipeline#invoke :#{name} expects #{@dispatcher.class.name} to declare a `dependency :#{name}, ...`"
|
|
@@ -132,6 +82,14 @@ module Hubbado
|
|
|
132
82
|
@dispatcher.send(name).(@ctx, *args, **kwargs)
|
|
133
83
|
end
|
|
134
84
|
|
|
85
|
+
def record(name, return_value)
|
|
86
|
+
if return_value.is_a?(Result) && return_value.failure?
|
|
87
|
+
@failed_result = tag_failure(return_value, name)
|
|
88
|
+
else
|
|
89
|
+
@trail << name
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
135
93
|
def tag_failure(result, step_name)
|
|
136
94
|
tagged_error = result.error.merge(step: step_name)
|
|
137
95
|
Result.fail(result.ctx, error: tagged_error, trail: @trail.dup, i18n_scope: result.i18n_scope)
|
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.4.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-15 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: evt-casing
|