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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bf9d0a82e802923d5f0a04043400578eb8c90ad8c78a00073a30c99cab8784e6
4
- data.tar.gz: 91e67b7ed3f8fc5fe428bcfea0eb359d4f8c03edb090ccba56d1a16799d2a48d
3
+ metadata.gz: 225c967e73e28a28effbc26e9c3bbdfce3357932a8374046dc46056d0dd8dd7d
4
+ data.tar.gz: 7a446faedf4bb88018c17a6130f29ec85f1fc354929864aaa2dd55f86753ffca
5
5
  SHA512:
6
- metadata.gz: 902c46876f2df354ce46f340ef0fa6432d34c867eaff764c14e5ab54f54a8bb1dc466601c1c3da8f3823d5c0637af3d5c279e7bdb3596c8ff6a785488a333d31
7
- data.tar.gz: 9edff4e5aa8e45dbdb20a31f7cfe360c6f03f8751986cef344146be16e5d5e771c1c8318ef89ab80baef61b89927aabe327ac29c175bbdc7246e00257db8086d
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 three step shapes
117
+ ## The two step shapes
118
118
 
119
119
  ```ruby
120
120
  pipeline(ctx) do |p|
121
- p.invoke(:find, :user) # declared dependency (macro or sequencer)
122
- p.step(:scrub_params) # local method `def scrub_params(ctx)`
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. Auto-dispatches to
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
- The `pipeline(ctx)` helper (lowercase `p`) is what enables blockless
135
- `p.step(:foo)` auto-dispatch it builds a Pipeline that knows which
136
- sequencer to dispatch back to. `Pipeline.(ctx)` (capital `P`) is the bare
137
- constructor with no dispatcher and requires every `step` to have a block.
138
- Use `pipeline(ctx)` inside a sequencer; `Pipeline.(ctx)` is mainly useful
139
- for framework tests.
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) { |c| UserMailer.updated(c[:user]).deliver_later }
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
@@ -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.3.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
- # `Pipeline.(ctx) { |p| ... }` is the block form: yields the pipeline,
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) { |ctx| ... }` runs the block. `step(:name)` with no
35
- # block dispatches to `dispatcher.send(name, ctx)` on the sequencer
36
- # that built this pipeline (via the mixin's `pipeline(ctx)` helper).
37
- # Block beats dispatch when both are available; raises if neither.
38
- #
39
- # Lenient return convention: a step is treated as successful unless it
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
- return_value = invoke_step(name, block)
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
- # sequencer: gets the dependency via `dispatcher.send(name)` (the
60
- # reader), then invokes it with `(ctx, *args, **kwargs)`. Same trail
61
- # recording, failure short-circuiting, and lenient return convention as
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 (`Macros::Model::Find`)
65
- # and nested sequencers (`Seqs::Present`) alike. Use `step` for local
66
- # instance methods like `def deserialize_contract(ctx)`.
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
- return_value = invoke_dependency(name, args, kwargs)
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(&block)
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, block)
107
- if block
108
- block.call(@ctx)
109
- elsif @dispatcher
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.3.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-14 00:00:00.000000000 Z
11
+ date: 2026-05-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: evt-casing