standard-procedure-plumbing 0.1.2 → 0.2.1
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/.vscode/tasks.json +11 -0
- data/CHANGELOG.md +11 -0
- data/README.md +79 -43
- data/checksums/standard-procedure-plumbing-0.2.0.gem.sha512 +1 -0
- data/checksums/standard-procedure-plumbing-0.2.1.gem.sha512 +1 -0
- data/lib/plumbing/error.rb +2 -2
- data/lib/plumbing/event_dispatcher/fiber.rb +61 -0
- data/lib/plumbing/event_dispatcher.rb +35 -0
- data/lib/plumbing/filter.rb +9 -18
- data/lib/plumbing/junction.rb +21 -0
- data/lib/plumbing/pipe.rb +60 -15
- data/lib/plumbing/{chain.rb → pipeline/contracts.rb} +11 -24
- data/lib/plumbing/pipeline/operations.rb +45 -0
- data/lib/plumbing/pipeline.rb +14 -0
- data/lib/plumbing/version.rb +1 -1
- data/lib/plumbing.rb +9 -6
- metadata +14 -7
- data/lib/plumbing/blocked_pipe.rb +0 -105
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 00f89cef3ca3db2daf437446af1788755906434de2ec2d4e6e81130b26190f84
|
4
|
+
data.tar.gz: 44d27c6f52de7bbc24c488f741035d0a9fe4341036c423e51b5031896ca9f50a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6785c35e5596df5718a8ce458ced0713abbe3d18516a90b6353c806da8f867d3bf4d48d70b8b31b15b7c23a491fa2c855c091250aa96bdf97595f813b53e8e18
|
7
|
+
data.tar.gz: 22169d08af47f789dd5a950635ad46598c6a77f402a2221ed719f369df00b065185b402f14ba2dd2dd82b54cac8774d67d1adc2c07524307a644be11707d7956
|
data/.vscode/tasks.json
ADDED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,14 @@
|
|
1
|
+
## [0.2.1] - 2024-08-25
|
2
|
+
|
3
|
+
- Split the Pipe implementation between the Pipe and EventDispatcher
|
4
|
+
- Use different EventDispatchers to handle fibers or inline pipes
|
5
|
+
- Renamed Chain to Pipeline
|
6
|
+
|
7
|
+
## [0.2.0] - 2024-08-14
|
8
|
+
|
9
|
+
- Added optional Dry::Validation support
|
10
|
+
- Use Async for fiber-based pipes
|
11
|
+
|
1
12
|
## [0.1.2] - 2024-08-14
|
2
13
|
|
3
14
|
- Removed dependencies
|
data/README.md
CHANGED
@@ -1,5 +1,63 @@
|
|
1
1
|
# Plumbing
|
2
2
|
|
3
|
+
## Plumbing::Pipeline - transform data through a pipeline
|
4
|
+
|
5
|
+
Define a sequence of operations that proceed in order, passing their output from one operation as the input to another.
|
6
|
+
|
7
|
+
Use `perform` to define a step that takes some input and returns a different output.
|
8
|
+
Use `execute` to define a step that takes some input and returns that same input.
|
9
|
+
Use `embed` to define a step that uses another `Plumbing::Chain` class to generate the output.
|
10
|
+
|
11
|
+
If you have [dry-validation](https://dry-rb.org/gems/dry-validation/1.10/) installed, you can validate your input using a `Dry::Validation::Contract`.
|
12
|
+
|
13
|
+
If you don't want to use dry-validation, you can instead define a `pre_condition` (although there's nothing to stop you defining a contract as well as pre_conditions - with the contract being verified first).
|
14
|
+
|
15
|
+
You can also verify that the output generated is as expected by defining a `post_condition`.
|
16
|
+
|
17
|
+
### Usage:
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
require "plumbing"
|
21
|
+
class BuildSequence < Plumbing::Pipeline
|
22
|
+
pre_condition :must_be_an_array do |input|
|
23
|
+
# you could replace this with a `validate` definition (using a Dry::Validation::Contract) if you prefer
|
24
|
+
input.is_a? Array
|
25
|
+
end
|
26
|
+
|
27
|
+
post_condition :must_have_three_elements do |output|
|
28
|
+
# this is a stupid post-condition but 🤷🏾♂️, this is just an example
|
29
|
+
output.length == 3
|
30
|
+
end
|
31
|
+
|
32
|
+
perform :add_first
|
33
|
+
perform :add_second
|
34
|
+
perform :add_third
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def add_first input
|
39
|
+
input << "first"
|
40
|
+
end
|
41
|
+
|
42
|
+
def add_second input
|
43
|
+
input << "second"
|
44
|
+
end
|
45
|
+
|
46
|
+
def add_third input
|
47
|
+
input << "third"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
BuildSequence.new.call []
|
52
|
+
# => ["first", "second", "third"]
|
53
|
+
|
54
|
+
BuildSequence.new.call 1
|
55
|
+
# => Plumbing::PreconditionError("must_be_an_array")
|
56
|
+
|
57
|
+
BuildSequence.new.call ["extra element"]
|
58
|
+
# => Plumbing::PostconditionError("must_have_three_elements")
|
59
|
+
```
|
60
|
+
|
3
61
|
## Plumbing::Pipe - a composable observer
|
4
62
|
|
5
63
|
[Observers](https://ruby-doc.org/3.3.0/stdlibs/observer/Observable.html) in Ruby are a pattern where objects (observers) register their interest in another object (the observable). This pattern is common throughout programming languages (event listeners in Javascript, the dependency protocol in [Smalltalk](https://en.wikipedia.org/wiki/Smalltalk)).
|
@@ -28,7 +86,9 @@ require "plumbing"
|
|
28
86
|
|
29
87
|
@source = Plumbing::Pipe.start
|
30
88
|
|
31
|
-
@filter = Plumbing::Filter.start source: @source
|
89
|
+
@filter = Plumbing::Filter.start source: @source do |event|
|
90
|
+
%w[important urgent].include? event.type
|
91
|
+
end
|
32
92
|
|
33
93
|
@observer = @filter.add_observer do |event|
|
34
94
|
puts event.type
|
@@ -83,9 +143,9 @@ require "plumbing"
|
|
83
143
|
@first_source = Plumbing::Pipe.start
|
84
144
|
@second_source = Plumbing::Pipe.start
|
85
145
|
|
86
|
-
@
|
146
|
+
@junction = Plumbing::Junction.start @first_source, @second_source
|
87
147
|
|
88
|
-
@observer = @
|
148
|
+
@observer = @junction.add_observer do |event|
|
89
149
|
puts event.type
|
90
150
|
end
|
91
151
|
|
@@ -95,55 +155,31 @@ end
|
|
95
155
|
# => "two"
|
96
156
|
```
|
97
157
|
|
98
|
-
|
99
|
-
|
100
|
-
Define a sequence of operations that proceed in order, passing their output from one operation as the input to another.
|
101
|
-
|
102
|
-
You can define pre-conditions (which validate the inputs supplied) or post-conditions (which validate the output).
|
103
|
-
|
104
|
-
### Usage:
|
105
|
-
|
158
|
+
Dispatching events asynchronously (using Fibers)
|
106
159
|
```ruby
|
107
160
|
require "plumbing"
|
108
|
-
|
109
|
-
|
110
|
-
input.is_a? Array
|
111
|
-
end
|
161
|
+
require "plumbing/event_dispatcher/fiber"
|
162
|
+
require "async"
|
112
163
|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
end
|
164
|
+
# `limit` controls how many fibers can dispatch events concurrently - the default is 4
|
165
|
+
@first_source = Plumbing::Pipe.start dispatcher: Plumbing::EventDispatcher::Fiber.new limit: 8
|
166
|
+
@second_source = Plumbing::Pipe.start dispatcher: Plumbing::EventDispatcher::Fiber.new limit: 2
|
117
167
|
|
118
|
-
|
119
|
-
perform :add_second
|
120
|
-
perform :add_third
|
121
|
-
|
122
|
-
private
|
168
|
+
@junction = Plumbing::Junction.start @first_source, @second_source, dispatcher: Plumbing::EventDispatcher::Fiber.new
|
123
169
|
|
124
|
-
|
125
|
-
|
126
|
-
end
|
127
|
-
|
128
|
-
def add_second input
|
129
|
-
input << "second"
|
130
|
-
end
|
131
|
-
|
132
|
-
def add_third input
|
133
|
-
input << "third"
|
134
|
-
end
|
170
|
+
@filter = Plumbing::Filter.start source: @junction, dispatcher: Plumbing::EventDispatcher::Fibernew do |event|
|
171
|
+
%w[one-one two-two].include? event.type
|
135
172
|
end
|
136
173
|
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
BuildSequence.new.call ["extra element"]
|
144
|
-
# => Plumbing::PostconditionError("must_have_three_elements")
|
174
|
+
Sync do
|
175
|
+
@first_source.notify "one-one"
|
176
|
+
@first_source.notify "one-two"
|
177
|
+
@second_source.notify "two-one"
|
178
|
+
@second_source.notify "two-two"
|
179
|
+
end
|
145
180
|
```
|
146
181
|
|
182
|
+
|
147
183
|
## Installation
|
148
184
|
|
149
185
|
Install the gem and add to the application's Gemfile by executing:
|
@@ -0,0 +1 @@
|
|
1
|
+
f8972f9d59a7a040262dd738514a53e38300604b524f67a16bfc465d69335838fd6861a429c164764f72a4172d54462c8816eeb43d4bafe8099c97eb9fe7b156
|
@@ -0,0 +1 @@
|
|
1
|
+
92f63ff4b002a27778854ba86d2bb4b60e4ce7eae93592d3c3e3f90aa03045d961b6dfea86901a7290485d4953df548c521ae49efd7d53547939208515aa392e
|
data/lib/plumbing/error.rb
CHANGED
@@ -14,6 +14,6 @@ module Plumbing
|
|
14
14
|
# Error raised because an invalid observer was registered
|
15
15
|
class InvalidObserver < Error; end
|
16
16
|
|
17
|
-
# Error raised because a
|
18
|
-
class
|
17
|
+
# Error raised because a Pipe was connected to a non-Pipe
|
18
|
+
class InvalidSource < Plumbing::Error; end
|
19
19
|
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require "async/task"
|
2
|
+
require "async/semaphore"
|
3
|
+
|
4
|
+
module Plumbing
|
5
|
+
class EventDispatcher
|
6
|
+
class Fiber < EventDispatcher
|
7
|
+
def initialize limit: 4
|
8
|
+
super()
|
9
|
+
@semaphore = Async::Semaphore.new(limit)
|
10
|
+
@queue = Set.new
|
11
|
+
@paused = false
|
12
|
+
end
|
13
|
+
|
14
|
+
def dispatch event
|
15
|
+
@queue << event
|
16
|
+
dispatch_events unless @paused
|
17
|
+
end
|
18
|
+
|
19
|
+
def pause
|
20
|
+
@paused = true
|
21
|
+
end
|
22
|
+
|
23
|
+
def resume
|
24
|
+
@paused = false
|
25
|
+
dispatch_events
|
26
|
+
end
|
27
|
+
|
28
|
+
def queue_size
|
29
|
+
@queue.size
|
30
|
+
end
|
31
|
+
|
32
|
+
def shutdown
|
33
|
+
super
|
34
|
+
@queue.clear
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def dispatch_events
|
40
|
+
@semaphore.async do |task|
|
41
|
+
events = @queue.dup
|
42
|
+
@queue.clear
|
43
|
+
events.each do |event|
|
44
|
+
dispatch_event event, task
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def dispatch_event event, task
|
50
|
+
@observers.collect do |observer|
|
51
|
+
task.async do
|
52
|
+
observer.call event
|
53
|
+
rescue => ex
|
54
|
+
puts ex
|
55
|
+
ex
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Plumbing
|
2
|
+
class EventDispatcher
|
3
|
+
def initialize observers: []
|
4
|
+
@observers = observers
|
5
|
+
end
|
6
|
+
|
7
|
+
def add_observer observer = nil, &block
|
8
|
+
observer ||= block.to_proc
|
9
|
+
raise Plumbing::InvalidObserver.new "observer_does_not_respond_to_call" unless observer.respond_to? :call
|
10
|
+
@observers << observer
|
11
|
+
observer
|
12
|
+
end
|
13
|
+
|
14
|
+
def remove_observer observer
|
15
|
+
@observers.delete observer
|
16
|
+
end
|
17
|
+
|
18
|
+
def is_observer? observer
|
19
|
+
@observers.include? observer
|
20
|
+
end
|
21
|
+
|
22
|
+
def dispatch event
|
23
|
+
@observers.collect do |observer|
|
24
|
+
observer.call event
|
25
|
+
rescue => ex
|
26
|
+
puts ex
|
27
|
+
ex
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def shutdown
|
32
|
+
@observers = []
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/plumbing/filter.rb
CHANGED
@@ -1,31 +1,22 @@
|
|
1
|
-
require_relative "blocked_pipe"
|
2
|
-
|
3
1
|
module Plumbing
|
4
2
|
# A pipe that filters events from a source pipe
|
5
|
-
class Filter <
|
6
|
-
class InvalidFilter < Error; end
|
7
|
-
|
3
|
+
class Filter < Pipe
|
8
4
|
# Chain this pipe to the source pipe
|
9
|
-
# @param source [Plumbing::
|
10
|
-
# @param accepts [
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
raise InvalidFilter.new "accepts and rejects must be arrays" unless accepts.is_a?(Array) && rejects.is_a?(Array)
|
16
|
-
@accepted_event_types = accepts
|
17
|
-
@rejected_event_types = rejects
|
5
|
+
# @param source [Plumbing::Pipe]
|
6
|
+
# @param &accepts [Block] a block that returns a boolean value - true to accept the event, false to reject it
|
7
|
+
def initialize source:, dispatcher: nil, &accepts
|
8
|
+
super(dispatcher: dispatcher)
|
9
|
+
raise InvalidSource.new "#{source} must be a Plumbing::Pipe descendant" unless source.is_a? Plumbing::Pipe
|
10
|
+
@accepts = accepts
|
18
11
|
source.add_observer do |event|
|
19
|
-
filter_and_republish
|
12
|
+
filter_and_republish event
|
20
13
|
end
|
21
14
|
end
|
22
15
|
|
23
16
|
private
|
24
17
|
|
25
18
|
def filter_and_republish event
|
26
|
-
|
27
|
-
return nil if @accepted_event_types.any? && !@accepted_event_types.include?(event.type)
|
28
|
-
return nil if @rejected_event_types.include? event.type
|
19
|
+
return nil unless @accepts.call event
|
29
20
|
dispatch event
|
30
21
|
end
|
31
22
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Plumbing
|
2
|
+
# A pipe that filters events from a source pipe
|
3
|
+
class Junction < Pipe
|
4
|
+
# Chain multiple sources to this pipe
|
5
|
+
# @param [Array<Plumbing::Pipe>]
|
6
|
+
def initialize *sources, dispatcher: nil
|
7
|
+
super(dispatcher: dispatcher)
|
8
|
+
@sources = sources.collect { |source| add(source) }
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def add source
|
14
|
+
raise InvalidSource.new "#{source} must be a Plumbing::Pipe descendant" unless source.is_a? Plumbing::Pipe
|
15
|
+
source.add_observer do |event|
|
16
|
+
dispatch event
|
17
|
+
end
|
18
|
+
source
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/plumbing/pipe.rb
CHANGED
@@ -1,29 +1,74 @@
|
|
1
|
-
require_relative "blocked_pipe"
|
2
|
-
|
3
1
|
module Plumbing
|
4
|
-
#
|
5
|
-
class Pipe
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
2
|
+
# A basic pipe
|
3
|
+
class Pipe
|
4
|
+
require_relative "event_dispatcher"
|
5
|
+
|
6
|
+
# Subclasses should call `super()` to ensure the pipe is initialised corrected
|
7
|
+
def initialize dispatcher: nil
|
8
|
+
@dispatcher = dispatcher || EventDispatcher.new
|
11
9
|
end
|
12
10
|
|
11
|
+
# Push an event into the pipe
|
12
|
+
# @param event [Plumbing::Event] the event to push into the pipe
|
13
13
|
def << event
|
14
|
-
raise Plumbing::InvalidEvent.new
|
15
|
-
|
14
|
+
raise Plumbing::InvalidEvent.new event unless event.is_a? Plumbing::Event
|
15
|
+
dispatch event
|
16
|
+
end
|
17
|
+
|
18
|
+
# A shortcut to creating and then pushing an event
|
19
|
+
# @param event_type [String] representing the type of event this is
|
20
|
+
# @param data [Hash] representing the event-specific data to be passed to the observers
|
21
|
+
def notify event_type, data = nil
|
22
|
+
Event.new(type: event_type, data: data).tap do |event|
|
23
|
+
self << event
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Add an observer to this pipe
|
28
|
+
# @param callable [Proc] (optional)
|
29
|
+
# @param &block [Block] (optional)
|
30
|
+
# @return an object representing this observer (dependent upon the implementation of the pipe itself)
|
31
|
+
# Either a `callable` or a `block` must be supplied. If the latter, it is converted to a [Proc]
|
32
|
+
def add_observer(observer = nil, &)
|
33
|
+
@dispatcher.add_observer(observer, &)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Remove an observer from this pipe
|
37
|
+
# @param observer
|
38
|
+
# This removes the given observer from this pipe. The observer should have previously been returned by #add_observer and is implementation-specific
|
39
|
+
def remove_observer observer
|
40
|
+
@dispatcher.remove_observer observer
|
16
41
|
end
|
17
42
|
|
43
|
+
# Test whether the given observer is observing this pipe
|
44
|
+
# @param observer
|
45
|
+
# @return [boolean]
|
46
|
+
def is_observer? observer
|
47
|
+
@dispatcher.is_observer? observer
|
48
|
+
end
|
49
|
+
|
50
|
+
# Close this pipe and perform any cleanup.
|
51
|
+
# Subclasses should override this to perform their own shutdown routines and call `super` to ensure everything is tidied up
|
18
52
|
def shutdown
|
19
|
-
|
20
|
-
@
|
53
|
+
# clean up and release any observers, just in case
|
54
|
+
@dispatcher.shutdown
|
55
|
+
end
|
56
|
+
|
57
|
+
# Start this pipe
|
58
|
+
# Subclasses may override this method to add any implementation specific details.
|
59
|
+
# By default any supplied parameters are called to the subclass' `initialize` method
|
60
|
+
def self.start(*, **, &)
|
61
|
+
new(*, **, &)
|
21
62
|
end
|
22
63
|
|
23
64
|
protected
|
24
65
|
|
25
|
-
|
26
|
-
|
66
|
+
# Dispatch an event to all observers
|
67
|
+
# @param event [Plumbing::Event]
|
68
|
+
# Enumerates all observers and `calls` them with this event
|
69
|
+
# Discards any errors raised by the observer so that all observers will be successfully notified
|
70
|
+
def dispatch event
|
71
|
+
@dispatcher.dispatch event
|
27
72
|
end
|
28
73
|
end
|
29
74
|
end
|
@@ -1,40 +1,20 @@
|
|
1
1
|
module Plumbing
|
2
|
-
|
3
|
-
|
4
|
-
def call params
|
5
|
-
self.class._call params, self
|
6
|
-
end
|
7
|
-
|
8
|
-
class << self
|
2
|
+
class Pipeline
|
3
|
+
module Contracts
|
9
4
|
def pre_condition name, &validator
|
10
5
|
pre_conditions[name.to_sym] = validator
|
11
6
|
end
|
12
7
|
|
13
|
-
def
|
14
|
-
|
15
|
-
operations << implementation
|
8
|
+
def validate_with contract_class
|
9
|
+
@validation_contract = contract_class
|
16
10
|
end
|
17
11
|
|
18
12
|
def post_condition name, &validator
|
19
13
|
post_conditions[name.to_sym] = validator
|
20
14
|
end
|
21
15
|
|
22
|
-
def _call params, instance
|
23
|
-
validate_preconditions_for params
|
24
|
-
result = params
|
25
|
-
operations.each do |operation|
|
26
|
-
result = operation.call(result, instance)
|
27
|
-
end
|
28
|
-
validate_postconditions_for result
|
29
|
-
result
|
30
|
-
end
|
31
|
-
|
32
16
|
private
|
33
17
|
|
34
|
-
def operations
|
35
|
-
@operations ||= []
|
36
|
-
end
|
37
|
-
|
38
18
|
def pre_conditions
|
39
19
|
@pre_conditions ||= {}
|
40
20
|
end
|
@@ -43,6 +23,13 @@ module Plumbing
|
|
43
23
|
@post_conditions ||= {}
|
44
24
|
end
|
45
25
|
|
26
|
+
def validate_contract_for input
|
27
|
+
return true if @validation_contract.nil?
|
28
|
+
result = const_get(@validation_contract).new.call(input)
|
29
|
+
raise PreConditionError, result.errors.to_h.to_yaml unless result.success?
|
30
|
+
input
|
31
|
+
end
|
32
|
+
|
46
33
|
def validate_preconditions_for input
|
47
34
|
failed_preconditions = pre_conditions.select { |name, validator| !validator.call(input) }
|
48
35
|
raise PreConditionError, failed_preconditions.keys.join(", ") if failed_preconditions.any?
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Plumbing
|
2
|
+
class Pipeline
|
3
|
+
module Operations
|
4
|
+
def perform method, using: nil, &implementation
|
5
|
+
using.nil? ? perform_internal(method, &implementation) : perform_external(method, using)
|
6
|
+
end
|
7
|
+
|
8
|
+
def execute method
|
9
|
+
implementation ||= ->(input, instance) do
|
10
|
+
instance.send(method, input)
|
11
|
+
input
|
12
|
+
end
|
13
|
+
operations << implementation
|
14
|
+
end
|
15
|
+
|
16
|
+
def _call input, instance
|
17
|
+
validate_contract_for input
|
18
|
+
validate_preconditions_for input
|
19
|
+
result = input
|
20
|
+
operations.each do |operation|
|
21
|
+
result = operation.call(result, instance)
|
22
|
+
end
|
23
|
+
validate_postconditions_for result
|
24
|
+
result
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def operations
|
30
|
+
@operations ||= []
|
31
|
+
end
|
32
|
+
|
33
|
+
def perform_internal method, &implementation
|
34
|
+
implementation ||= ->(input, instance) { instance.send(method, input) }
|
35
|
+
operations << implementation
|
36
|
+
end
|
37
|
+
|
38
|
+
def perform_external method, class_or_class_name
|
39
|
+
external_class = class_or_class_name.is_a?(String) ? const_get(class_or_class_name) : class_or_class_name
|
40
|
+
implementation = ->(input, instance) { external_class.new.call(input) }
|
41
|
+
operations << implementation
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require_relative "pipeline/contracts"
|
2
|
+
require_relative "pipeline/operations"
|
3
|
+
|
4
|
+
module Plumbing
|
5
|
+
# A chain of operations that are executed in sequence
|
6
|
+
class Pipeline
|
7
|
+
extend Plumbing::Pipeline::Contracts
|
8
|
+
extend Plumbing::Pipeline::Operations
|
9
|
+
|
10
|
+
def call input
|
11
|
+
self.class._call input, self
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/lib/plumbing/version.rb
CHANGED
data/lib/plumbing.rb
CHANGED
@@ -1,10 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
require_relative "plumbing/version"
|
4
|
+
|
5
|
+
require_relative "plumbing/error"
|
6
|
+
require_relative "plumbing/event"
|
7
|
+
require_relative "plumbing/pipe"
|
8
|
+
require_relative "plumbing/filter"
|
9
|
+
require_relative "plumbing/junction"
|
10
|
+
require_relative "plumbing/pipeline"
|
5
11
|
|
6
|
-
|
7
|
-
require_relative "plumbing/event"
|
8
|
-
require_relative "plumbing/pipe"
|
9
|
-
require_relative "plumbing/chain"
|
12
|
+
module Plumbing
|
10
13
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: standard-procedure-plumbing
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rahoul Baruah
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-08-
|
11
|
+
date: 2024-08-25 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: A composable event pipeline and sequential pipelines of operations
|
14
14
|
email:
|
@@ -21,6 +21,7 @@ files:
|
|
21
21
|
- ".rubocop.yml"
|
22
22
|
- ".solargraph.yml"
|
23
23
|
- ".standard.yml"
|
24
|
+
- ".vscode/tasks.json"
|
24
25
|
- CHANGELOG.md
|
25
26
|
- CODE_OF_CONDUCT.md
|
26
27
|
- LICENSE
|
@@ -28,20 +29,26 @@ files:
|
|
28
29
|
- Rakefile
|
29
30
|
- checksums/standard-procedure-plumbing-0.1.1.gem.sha512
|
30
31
|
- checksums/standard-procedure-plumbing-0.1.2.gem.sha512
|
32
|
+
- checksums/standard-procedure-plumbing-0.2.0.gem.sha512
|
33
|
+
- checksums/standard-procedure-plumbing-0.2.1.gem.sha512
|
31
34
|
- lib/plumbing.rb
|
32
|
-
- lib/plumbing/blocked_pipe.rb
|
33
|
-
- lib/plumbing/chain.rb
|
34
35
|
- lib/plumbing/error.rb
|
35
36
|
- lib/plumbing/event.rb
|
37
|
+
- lib/plumbing/event_dispatcher.rb
|
38
|
+
- lib/plumbing/event_dispatcher/fiber.rb
|
36
39
|
- lib/plumbing/filter.rb
|
40
|
+
- lib/plumbing/junction.rb
|
37
41
|
- lib/plumbing/pipe.rb
|
42
|
+
- lib/plumbing/pipeline.rb
|
43
|
+
- lib/plumbing/pipeline/contracts.rb
|
44
|
+
- lib/plumbing/pipeline/operations.rb
|
38
45
|
- lib/plumbing/version.rb
|
39
46
|
- sig/plumbing.rbs
|
40
|
-
homepage: https://
|
47
|
+
homepage: https://github.com/standard-procedure/plumbing
|
41
48
|
licenses: []
|
42
49
|
metadata:
|
43
50
|
allowed_push_host: https://rubygems.org
|
44
|
-
homepage_uri: https://
|
51
|
+
homepage_uri: https://github.com/standard-procedure/plumbing
|
45
52
|
source_code_uri: https://github.com/standard-procedure/plumbing
|
46
53
|
changelog_uri: https://github.com/standard-procedure/plumbing/blob/main/CHANGELOG.md
|
47
54
|
post_install_message:
|
@@ -59,7 +66,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
59
66
|
- !ruby/object:Gem::Version
|
60
67
|
version: '0'
|
61
68
|
requirements: []
|
62
|
-
rubygems_version: 3.5.
|
69
|
+
rubygems_version: 3.5.17
|
63
70
|
signing_key:
|
64
71
|
specification_version: 4
|
65
72
|
summary: Plumbing - various pipelines for your ruby application
|
@@ -1,105 +0,0 @@
|
|
1
|
-
require_relative "error"
|
2
|
-
require_relative "event"
|
3
|
-
|
4
|
-
module Plumbing
|
5
|
-
# The "plumbing" for a Pipe.
|
6
|
-
# This class is "blocked", in that it won't push any events to registered observers.
|
7
|
-
# Instead, this is the basis for subclasses like [Plumbing::Pipe] which actually allow events to flow through them.
|
8
|
-
class BlockedPipe
|
9
|
-
# Create a new BlockedPipe
|
10
|
-
# Subclasses should call `super()` to ensure the pipe is initialised corrected
|
11
|
-
def initialize
|
12
|
-
@observers = []
|
13
|
-
end
|
14
|
-
|
15
|
-
# Push an event into the pipe
|
16
|
-
# @param event [Plumbing::Event] the event to push into the pipe
|
17
|
-
# Subclasses should implement this method
|
18
|
-
def << event
|
19
|
-
raise Plumbing::PipeIsBlocked
|
20
|
-
end
|
21
|
-
|
22
|
-
# A shortcut to creating and then pushing an event
|
23
|
-
# @param event_type [String] representing the type of event this is
|
24
|
-
# @param data [Hash] representing the event-specific data to be passed to the observers
|
25
|
-
def notify event_type, data = nil
|
26
|
-
Event.new(type: event_type, data: data).tap do |event|
|
27
|
-
self << event
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
# Add an observer to this pipe
|
32
|
-
# @param callable [Proc] (optional)
|
33
|
-
# @param &block [Block] (optional)
|
34
|
-
# @return an object representing this observer (dependent upon the implementation of the pipe itself)
|
35
|
-
# Either a `callable` or a `block` must be supplied. If the latter, it is converted to a [Proc]
|
36
|
-
def add_observer observer = nil, &block
|
37
|
-
observer ||= block.to_proc
|
38
|
-
raise Plumbing::InvalidObserver.new "observer_does_not_respond_to_call" unless observer.respond_to? :call
|
39
|
-
@observers << observer
|
40
|
-
end
|
41
|
-
|
42
|
-
# Remove an observer from this pipe
|
43
|
-
# @param observer
|
44
|
-
# This removes the given observer from this pipe. The observer should have previously been returned by #add_observer and is implementation-specific
|
45
|
-
def remove_observer observer
|
46
|
-
@observers.delete observer
|
47
|
-
end
|
48
|
-
|
49
|
-
# Test whether the given observer is observing this pipe
|
50
|
-
# @param observer
|
51
|
-
# @return [boolean]
|
52
|
-
def is_observer? observer
|
53
|
-
@observers.include? observer
|
54
|
-
end
|
55
|
-
|
56
|
-
# Close this pipe and perform any cleanup.
|
57
|
-
# Subclasses should override this to perform their own shutdown routines and call `super` to ensure everything is tidied up
|
58
|
-
def shutdown
|
59
|
-
# clean up and release any observers, just in case
|
60
|
-
@observers = []
|
61
|
-
end
|
62
|
-
|
63
|
-
# Start this pipe
|
64
|
-
# Subclasses may override this method to add any implementation specific details.
|
65
|
-
# By default any supplied parameters are called to the subclass' `initialize` method
|
66
|
-
def self.start(**params)
|
67
|
-
new(**params)
|
68
|
-
end
|
69
|
-
|
70
|
-
protected
|
71
|
-
|
72
|
-
# Get the next event from the queue
|
73
|
-
# @return [Plumbing::Event]
|
74
|
-
# Subclasses should implement this method
|
75
|
-
def get_next_event
|
76
|
-
raise Plumbing::PipeIsBlocked
|
77
|
-
end
|
78
|
-
|
79
|
-
# Start the event loop
|
80
|
-
# This loop keeps running until `shutdown` is called
|
81
|
-
# Some subclasses may need to replace this method to deal with their own specific implementations
|
82
|
-
# @param initial_event [Plumbing::Event] optional; the first event in the queue
|
83
|
-
def start_run_loop initial_event = nil
|
84
|
-
loop do
|
85
|
-
event = initial_event || get_next_event
|
86
|
-
break if event == :shutdown
|
87
|
-
dispatch event
|
88
|
-
initial_event = nil
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
# Dispatch an event to all observers
|
93
|
-
# @param event [Plumbing::Event]
|
94
|
-
# Enumerates all observers and `calls` them with this event
|
95
|
-
# Discards any errors raised by the observer so that all observers will be successfully notified
|
96
|
-
def dispatch event
|
97
|
-
@observers.collect do |observer|
|
98
|
-
observer.call event
|
99
|
-
rescue => ex
|
100
|
-
puts ex
|
101
|
-
ex
|
102
|
-
end
|
103
|
-
end
|
104
|
-
end
|
105
|
-
end
|