cdc-core 0.1.1 → 0.1.2

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: a71182bac774b200c13556d0a887754a2788dc4e3581fc84a94288764900f365
4
- data.tar.gz: a67bb53791b19347fb0c589b59603abfc6f085504597531d7c9932da3a3d8749
3
+ metadata.gz: b6b50778bfa61fb46ffb973eaa1975345cdbd6df15d4a719110dba339b63ed68
4
+ data.tar.gz: e461bc281b0147d2c8fdcfc1e3c95f209076070c3585858cc97c5c90585cc6ad
5
5
  SHA512:
6
- metadata.gz: a96b523f2144fd94d0ecd633bf956fdcad791a994f3a2bf9e4559da9e4c713570f52c842e3cce15cb9a1491345190087c7cd1599612f5e4ecd5f933d22a6a9d6
7
- data.tar.gz: 44acef005734403fea47a88e8d142cf8f9667ef032b032af24dfc25cf25c491d1c2a6caf3fab3bb06fc7790d63b8c88a8cb9984e61803dac315cd6d8118a2957
6
+ metadata.gz: 761888ea92dcd0c5f0a268bdb999016de811486fd69a3e2eaab457d1d4b21e1a79664214017961da69ec05adebcc2d99b98e2b433430a8d69e3f70e2967be8ca
7
+ data.tar.gz: 27c091d4e1f0b16a1c3503cc12af85ed9311e9650f028251fbf2334d81849359b15932d92b556e4ebd11f1ef51f7b37bfdbfe630f79087ff50c450b049f3c17e
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.1.2] - 2026-06-10
4
+
5
+ - Added `CDC::Core::ProcessorChain` that feeds the successful value from one processor into the next processor
6
+ - Updated and enriched the API and Architecture documenation
7
+
3
8
  ## [0.1.1] - 2026-06-09
4
9
 
5
10
  ### Added
data/README.md CHANGED
@@ -15,7 +15,7 @@ Shared Change Data Capture vocabulary for Ruby.
15
15
  - Transaction grouping via `TransactionEnvelope`
16
16
  - Column-level change objects
17
17
  - Ordering vocabulary
18
- - Processor and composite processor contracts
18
+ - Processor, composite processor, processor chain, and pipeline contracts
19
19
  - Event filters
20
20
  - Small pipeline orchestration object
21
21
  - Router for supported work item shapes
@@ -186,7 +186,23 @@ AnalyticsProcessor.new.ractor_safe?
186
186
 
187
187
  This declares intent only. `cdc-core` does not execute processors in Ractors. `cdc-parallel` can use this signal before moving processor work across Ractors.
188
188
 
189
- ## Composite Processor
189
+ ## Downstream Workflow Primitives
190
+
191
+ `cdc-core` defines three small workflow primitives. Runtime gems and
192
+ application-specific integrations can execute these primitives without
193
+ inventing their own composition vocabulary.
194
+
195
+ ### CompositeProcessor
196
+
197
+ Use `CompositeProcessor` when many independent processors should receive the
198
+ same input.
199
+
200
+ ```text
201
+ event
202
+ ├─ AuditProcessor
203
+ ├─ AnalyticsProcessor
204
+ └─ WebhookProcessor
205
+ ```
190
206
 
191
207
  ```ruby
192
208
  processor = CDC::Core::CompositeProcessor.new([
@@ -197,7 +213,17 @@ processor = CDC::Core::CompositeProcessor.new([
197
213
  results = processor.process(event)
198
214
  ```
199
215
 
200
- ## Filters and Pipeline
216
+ ### Pipeline
217
+
218
+ Use `Pipeline` when one processor should run only after filters match.
219
+
220
+ ```text
221
+ event
222
+
223
+ filters
224
+
225
+ processor
226
+ ```
201
227
 
202
228
  ```ruby
203
229
  pipeline = CDC::Core::Pipeline.new(
@@ -211,6 +237,46 @@ pipeline = CDC::Core::Pipeline.new(
211
237
  result = pipeline.process(event)
212
238
  ```
213
239
 
240
+ ### ProcessorChain
241
+
242
+ Use `ProcessorChain` when each processor depends on the previous processor's
243
+ successful value.
244
+
245
+ ```text
246
+ user_ids
247
+
248
+ LoadUsersProcessor
249
+
250
+ users
251
+
252
+ SendNotificationsProcessor
253
+ ```
254
+
255
+ ```ruby
256
+ class LoadUsersProcessor < CDC::Core::Processor
257
+ def process(user_ids)
258
+ users = User.where(id: user_ids).to_a
259
+ CDC::Core::ProcessorResult.success(user_ids, value: users)
260
+ end
261
+ end
262
+
263
+ class SendNotificationsProcessor < CDC::Core::Processor
264
+ def process(users)
265
+ users.each { |user| NotificationMailer.notice(user).deliver_later }
266
+ CDC::Core::ProcessorResult.success(users, value: users.size)
267
+ end
268
+ end
269
+
270
+ chain = CDC::Core::ProcessorChain.new([
271
+ LoadUsersProcessor.new,
272
+ SendNotificationsProcessor.new
273
+ ])
274
+
275
+ result = chain.process([1, 2, 3])
276
+ result.value
277
+ # => 3
278
+ ```
279
+
214
280
  ## Non-goals
215
281
 
216
282
  `cdc-core` does not:
@@ -2,11 +2,12 @@
2
2
 
3
3
  module CDC
4
4
  module Core
5
- # Processor that delegates each event to multiple processors.
5
+ # Fan-out processor that delegates the same input to multiple processors.
6
6
  #
7
- # CompositeProcessor enables fan-out processing while preserving a simple
8
- # sequential execution model. It normalizes truthy/falsey processor returns
9
- # into ProcessorResult objects and can stop at the first failure.
7
+ # CompositeProcessor is for independent downstream side effects. Every
8
+ # configured processor receives the same input, and their results are
9
+ # collected independently. Use ProcessorChain when Processor B must receive
10
+ # Processor A's output.
10
11
  class CompositeProcessor < Processor
11
12
  # @return [Array<Processor>] processors executed for each event
12
13
  # @return [Boolean] whether processing stops on the first failure
@@ -2,11 +2,12 @@
2
2
 
3
3
  module CDC
4
4
  module Core
5
- # Connects filters with a processor to form an event-processing unit.
5
+ # Connects filters with one processor to form a guarded processing unit.
6
6
  #
7
- # A Pipeline first evaluates all filters. Matching events are handed to the
8
- # processor, while filtered events produce skipped results. Processor errors
9
- # are captured as failure results instead of escaping to the caller.
7
+ # A Pipeline evaluates all filters before invoking its processor. Matching
8
+ # inputs are processed, while filtered inputs produce skipped results. Use
9
+ # CompositeProcessor for fan-out to many processors and ProcessorChain for
10
+ # dependent step-by-step workflows.
10
11
  class Pipeline
11
12
  # @return [#process] processor invoked for matching events
12
13
  # @return [Array<Filter>] filters that must all match before processing
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CDC
4
+ module Core
5
+ # Sequential processor workflow where each successful result feeds the next processor.
6
+ #
7
+ # ProcessorChain models dependent workflows. Unlike CompositeProcessor,
8
+ # which sends the same input to every processor, ProcessorChain sends the
9
+ # value returned by one processor to the next processor. This is useful for
10
+ # downstream workflows such as loading records, enriching them, and then
11
+ # sending the enriched payload to a sink.
12
+ #
13
+ # Each processor result is normalized into a ProcessorResult. The chain stops
14
+ # at the first failure or skipped result because later processors depend on
15
+ # the previous processor's successful value.
16
+ class ProcessorChain < Processor
17
+ # @return [Array<#process>] processors executed in dependency order
18
+ # @return [Observer] observer notified of dispatch events
19
+ attr_reader :processors, :observer
20
+
21
+ # Build a processor chain.
22
+ #
23
+ # @param processors [Array<#process>] processors executed in dependency order
24
+ # @param observer [Observer, nil] instrumentation observer for each processor result
25
+ def initialize(processors, observer: NullObserver::INSTANCE) # rubocop:disable Lint/MissingSuper
26
+ @processors = processors.freeze
27
+ @observer = observer || NullObserver::INSTANCE
28
+ end
29
+
30
+ # Process one input through each processor in sequence.
31
+ #
32
+ # The first processor receives the original input. Each later processor
33
+ # receives the previous successful ProcessorResult#value. The returned
34
+ # value is the final ProcessorResult produced by the chain.
35
+ #
36
+ # @param input [Object] initial input for the first processor
37
+ # @return [ProcessorResult] final processor result or the first failed/skipped result
38
+ def process(input)
39
+ observer.dispatch_started(input)
40
+ current_input = input
41
+
42
+ processors.each do |processor|
43
+ result = process_with(processor, current_input)
44
+ observe_result(result)
45
+ return result unless result.success?
46
+
47
+ current_input = result.value
48
+ end
49
+
50
+ ProcessorResult.success(current_input, value: current_input)
51
+ end
52
+
53
+ # Process many inputs in order.
54
+ #
55
+ # @param inputs [Enumerable<Object>] inputs to process through the chain
56
+ # @return [Array<ProcessorResult>] final result for each input
57
+ def process_many(inputs)
58
+ inputs.map { |input| process(input) }.freeze
59
+ end
60
+
61
+ private
62
+
63
+ # Normalize processor return values into ProcessorResult objects.
64
+ #
65
+ # @param result [Object] raw processor result
66
+ # @param input [Object] input given to the processor
67
+ # @return [ProcessorResult] normalized result
68
+ def normalize_result(result, input)
69
+ return result if result.is_a?(ProcessorResult)
70
+
71
+ result ? ProcessorResult.success(input, value: result) : ProcessorResult.skipped(input)
72
+ end
73
+
74
+ def process_with(processor, input)
75
+ normalize_result(processor.process(input), input)
76
+ rescue StandardError => e
77
+ ProcessorResult.failure(e, event: input, processor: processor.class.name)
78
+ end
79
+
80
+ def observe_result(result)
81
+ case result.status
82
+ when :success
83
+ observer.dispatch_succeeded(result)
84
+ when :failure
85
+ observer.dispatch_failed(result)
86
+ when :skipped
87
+ observer.dispatch_skipped(result)
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -12,17 +12,19 @@ module CDC
12
12
  VALID_STATUSES = Ractor.make_shareable(%i[success failure skipped].freeze)
13
13
 
14
14
  # @return [Symbol] result status
15
- # @return [ChangeEvent, nil] event associated with the result
15
+ # @return [ChangeEvent, Object, nil] event or input associated with the result
16
+ # @return [Object, nil] value produced by the processor
16
17
  # @return [Exception, nil] failure error, when status is :failure
17
18
  # @return [EventMetadata] result metadata
18
- attr_reader :status, :event, :error, :metadata
19
+ attr_reader :status, :event, :value, :error, :metadata
19
20
 
20
21
  # Build a successful result.
21
22
  #
22
23
  # @param event [ChangeEvent, nil] processed event
23
24
  # @param metadata [Hash, EventMetadata] result metadata
25
+ # @param value [Object, nil] value produced by the processor; defaults to event for compatibility
24
26
  # @return [ProcessorResult]
25
- def self.success(event = nil, metadata: {}) = new(:success, event:, metadata:)
27
+ def self.success(event = nil, metadata: {}, value: event) = new(:success, event:, metadata:, value:)
26
28
 
27
29
  # Build a failure result.
28
30
  #
@@ -60,12 +62,14 @@ module CDC
60
62
  # @param event [ChangeEvent, nil] associated event
61
63
  # @param error [Exception, nil] associated failure
62
64
  # @param metadata [Hash, EventMetadata] result metadata
63
- def initialize(status, event: nil, error: nil, metadata: {})
65
+ # @param value [Object, nil] value produced by the processor
66
+ def initialize(status, event: nil, error: nil, metadata: {}, value: event)
64
67
  @status = normalize_status(status)
65
68
  @event = event
69
+ @value = value
66
70
  @error = error
67
71
  @metadata = metadata.is_a?(EventMetadata) ? metadata : EventMetadata.new(metadata)
68
- Ractor.make_shareable(self) unless error
72
+ make_shareable_when_possible
69
73
  end
70
74
 
71
75
  # @return [Boolean] true when status is :success
@@ -132,7 +136,8 @@ module CDC
132
136
  def to_h
133
137
  payload = {
134
138
  'status' => status,
135
- 'event' => event&.to_h,
139
+ 'event' => event.respond_to?(:to_h) ? event.to_h : event,
140
+ 'value' => value.respond_to?(:to_h) ? value.to_h : value,
136
141
  'error_class' => error_class,
137
142
  'error_message' => error_message,
138
143
  'error_backtrace' => error_backtrace,
@@ -144,6 +149,16 @@ module CDC
144
149
 
145
150
  private
146
151
 
152
+ def make_shareable_when_possible
153
+ Ractor.make_shareable(self) unless error
154
+ rescue Ractor::Error, TypeError
155
+ # ProcessorResult may carry application-specific values such as ActiveRecord
156
+ # result sets. Those values are valid in sequential/fiber runtimes even when
157
+ # they cannot be shared with Ractors. Ractor runtimes remain responsible for
158
+ # validating shareability at their boundary.
159
+ self
160
+ end
161
+
147
162
  def normalize_status(status)
148
163
  value = status.to_sym
149
164
  return value if VALID_STATUSES.include?(value)
@@ -3,6 +3,6 @@
3
3
  module CDC
4
4
  module Core
5
5
  # Current gem version.
6
- VERSION = '0.1.1'
6
+ VERSION = '0.1.2'
7
7
  end
8
8
  end
data/lib/cdc/core.rb CHANGED
@@ -15,6 +15,7 @@ require_relative 'core/source_adapter'
15
15
  require_relative 'core/processor_result'
16
16
  require_relative 'core/processor'
17
17
  require_relative 'core/composite_processor'
18
+ require_relative 'core/processor_chain'
18
19
  require_relative 'core/observer'
19
20
  require_relative 'core/null_observer'
20
21
  require_relative 'core/filter'
@@ -0,0 +1,45 @@
1
+ module CDC
2
+ module Core
3
+ # Sequential processor workflow where each successful result feeds the next processor.
4
+ class ProcessorChain < Processor
5
+ @processors: untyped
6
+ @observer: untyped
7
+
8
+ # @return [Array<#process>] processors executed in dependency order
9
+ attr_reader processors: untyped
10
+
11
+ # @return [Observer] observer notified of dispatch events
12
+ attr_reader observer: untyped
13
+
14
+ # Build a processor chain.
15
+ #
16
+ # @param processors [Array<#process>] processors executed in dependency order
17
+ # @param observer [Observer, nil] instrumentation observer for each processor result
18
+ def initialize: (untyped processors, ?observer: untyped?) -> void
19
+
20
+ # Process one input through each processor in sequence.
21
+ #
22
+ # @param input [Object] initial input for the first processor
23
+ # @return [ProcessorResult] final processor result or the first failed/skipped result
24
+ def process: (untyped input) -> untyped
25
+
26
+ # Process many inputs in order.
27
+ #
28
+ # @param inputs [Enumerable<Object>] inputs to process through the chain
29
+ # @return [Array<ProcessorResult>] final result for each input
30
+ def process_many: (untyped inputs) -> untyped
31
+
32
+ private
33
+
34
+ # Normalize processor return values into ProcessorResult objects.
35
+ #
36
+ # @param result [Object] raw processor result
37
+ # @param input [Object] input given to the processor
38
+ # @return [ProcessorResult] normalized result
39
+ def normalize_result: (untyped result, untyped input) -> untyped
40
+
41
+ def process_with: (untyped processor, untyped input) -> untyped
42
+ def observe_result: (untyped result) -> untyped
43
+ end
44
+ end
45
+ end
@@ -11,6 +11,8 @@ module CDC
11
11
 
12
12
  @event: untyped
13
13
 
14
+ @value: untyped
15
+
14
16
  @error: untyped
15
17
 
16
18
  @metadata: untyped
@@ -27,6 +29,9 @@ module CDC
27
29
  # @return [EventMetadata] result metadata
28
30
  attr_reader event: untyped
29
31
 
32
+ # @return [Object, nil] value produced by the processor
33
+ attr_reader value: untyped
34
+
30
35
  # @return [Symbol] result status
31
36
  # @return [ChangeEvent, nil] event associated with the result
32
37
  # @return [Exception, nil] failure error, when status is :failure
@@ -44,7 +49,7 @@ module CDC
44
49
  # @param event [ChangeEvent, nil] processed event
45
50
  # @param metadata [Hash, EventMetadata] result metadata
46
51
  # @return [ProcessorResult]
47
- def self.success: (?untyped? event, ?metadata: ::Hash[untyped, untyped]) -> untyped
52
+ def self.success: (?untyped? event, ?metadata: ::Hash[untyped, untyped], ?value: untyped?) -> untyped
48
53
 
49
54
  # Build a failure result.
50
55
  #
@@ -71,7 +76,7 @@ module CDC
71
76
  # @param event [ChangeEvent, nil] associated event
72
77
  # @param error [Exception, nil] associated failure
73
78
  # @param metadata [Hash, EventMetadata] result metadata
74
- def initialize: (untyped status, ?event: untyped?, ?error: untyped?, ?metadata: ::Hash[untyped, untyped]) -> void
79
+ def initialize: (untyped status, ?event: untyped?, ?error: untyped?, ?metadata: ::Hash[untyped, untyped], ?value: untyped?) -> void
75
80
 
76
81
  # @return [Boolean] true when status is :success
77
82
  def success?: () -> untyped
@@ -124,6 +129,8 @@ module CDC
124
129
 
125
130
  private
126
131
 
132
+ def make_shareable_when_possible: () -> untyped
133
+
127
134
  def normalize_status: (untyped status) -> untyped
128
135
  end
129
136
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cdc-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ken C. Demanawa
@@ -9,10 +9,9 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies: []
12
- description: 'CDC Core provides immutable, Ractor-safe Change Data Capture domain
13
- objects, processor contracts, filters, and pipeline primitives.
14
-
15
- '
12
+ description: |
13
+ CDC Core provides immutable, Ractor-safe Change Data Capture contracts and domain primitives,
14
+ including source adapters, change events, processors, routing, ordering policies, and pipelines.
16
15
  email:
17
16
  - kenneth.c.demanawa@gmail.com
18
17
  executables: []
@@ -38,6 +37,7 @@ files:
38
37
  - lib/cdc/core/ordering_scope.rb
39
38
  - lib/cdc/core/pipeline.rb
40
39
  - lib/cdc/core/processor.rb
40
+ - lib/cdc/core/processor_chain.rb
41
41
  - lib/cdc/core/processor_result.rb
42
42
  - lib/cdc/core/router.rb
43
43
  - lib/cdc/core/source_adapter.rb
@@ -60,6 +60,7 @@ files:
60
60
  - sig/cdc/core/ordering_scope.rbs
61
61
  - sig/cdc/core/pipeline.rbs
62
62
  - sig/cdc/core/processor.rbs
63
+ - sig/cdc/core/processor_chain.rbs
63
64
  - sig/cdc/core/processor_result.rbs
64
65
  - sig/cdc/core/router.rbs
65
66
  - sig/cdc/core/source_adapter.rbs
@@ -91,5 +92,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
91
92
  requirements: []
92
93
  rubygems_version: 3.6.9
93
94
  specification_version: 4
94
- summary: Database-agnostic Change Data Capture domain primitives for Ruby.
95
+ summary: Source-agnostic Change Data Capture contracts and domain primitives for Ruby..
95
96
  test_files: []