standard-procedure-plumbing 0.2.0 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.vscode/tasks.json +11 -0
- data/CHANGELOG.md +10 -0
- data/README.md +103 -45
- 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/checksums/standard-procedure-plumbing-0.2.2.gem.sha512 +1 -0
- data/lib/plumbing/error.rb +0 -6
- data/lib/plumbing/event_dispatcher/fiber.rb +61 -0
- data/lib/plumbing/event_dispatcher.rb +34 -0
- data/lib/plumbing/filter.rb +6 -13
- data/lib/plumbing/junction.rb +20 -0
- data/lib/plumbing/pipe.rb +12 -17
- data/lib/plumbing/{chain → pipeline}/contracts.rb +4 -4
- data/lib/plumbing/{chain → pipeline}/operations.rb +15 -10
- data/lib/plumbing/pipeline.rb +14 -0
- data/lib/plumbing/rubber_duck/object.rb +9 -0
- data/lib/plumbing/rubber_duck/proxy.rb +16 -0
- data/lib/plumbing/rubber_duck.rb +50 -0
- data/lib/plumbing/types.rb +6 -0
- data/lib/plumbing/version.rb +1 -1
- data/lib/plumbing.rb +9 -8
- metadata +17 -7
- data/lib/plumbing/chain.rb +0 -14
- data/lib/plumbing/fiber/pipe.rb +0 -36
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6074870313ece34eb4b9565602db4de70bb5b7e14e8a9051d85bb46b6fc64bf5
|
4
|
+
data.tar.gz: a81292b0ad9e87dfcce61d531e891c1b4ed25ccda3331ceb731c082c6e9c7c16
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 45827e921f7bc272e0688a6477d405a81e8c9ea64405078782fbf5aa45658066bf7f9c5503692f4f6eadfdbb08d21221eb4e0b0731654e4808e0f7ec1111f9bb
|
7
|
+
data.tar.gz: 1ba319545accf7051393845b1883d5eb69aeacba182c86f26ca6e95e7761711e922904d2ec5427872e8dcd6ccdf5fba25737fe5c20a887cc06ba23ab2184c81a
|
data/.vscode/tasks.json
ADDED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,13 @@
|
|
1
|
+
## [0.2.2] - 2024-08-25
|
2
|
+
|
3
|
+
- Added Plumbing::RubberDuck
|
4
|
+
|
5
|
+
## [0.2.1] - 2024-08-25
|
6
|
+
|
7
|
+
- Split the Pipe implementation between the Pipe and EventDispatcher
|
8
|
+
- Use different EventDispatchers to handle fibers or inline pipes
|
9
|
+
- Renamed Chain to Pipeline
|
10
|
+
|
1
11
|
## [0.2.0] - 2024-08-14
|
2
12
|
|
3
13
|
- Added optional Dry::Validation support
|
data/README.md
CHANGED
@@ -1,5 +1,64 @@
|
|
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
|
+
|
61
|
+
|
3
62
|
## Plumbing::Pipe - a composable observer
|
4
63
|
|
5
64
|
[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 +87,9 @@ require "plumbing"
|
|
28
87
|
|
29
88
|
@source = Plumbing::Pipe.start
|
30
89
|
|
31
|
-
@filter = Plumbing::Filter.start source: @source
|
90
|
+
@filter = Plumbing::Filter.start source: @source do |event|
|
91
|
+
%w[important urgent].include? event.type
|
92
|
+
end
|
32
93
|
|
33
94
|
@observer = @filter.add_observer do |event|
|
34
95
|
puts event.type
|
@@ -83,9 +144,9 @@ require "plumbing"
|
|
83
144
|
@first_source = Plumbing::Pipe.start
|
84
145
|
@second_source = Plumbing::Pipe.start
|
85
146
|
|
86
|
-
@
|
147
|
+
@junction = Plumbing::Junction.start @first_source, @second_source
|
87
148
|
|
88
|
-
@observer = @
|
149
|
+
@observer = @junction.add_observer do |event|
|
89
150
|
puts event.type
|
90
151
|
end
|
91
152
|
|
@@ -95,62 +156,60 @@ end
|
|
95
156
|
# => "two"
|
96
157
|
```
|
97
158
|
|
98
|
-
|
99
|
-
|
100
|
-
|
159
|
+
Dispatching events asynchronously (using Fibers)
|
160
|
+
```ruby
|
161
|
+
require "plumbing"
|
162
|
+
require "plumbing/event_dispatcher/fiber"
|
163
|
+
require "async"
|
101
164
|
|
102
|
-
|
103
|
-
|
104
|
-
|
165
|
+
# `limit` controls how many fibers can dispatch events concurrently - the default is 4
|
166
|
+
@first_source = Plumbing::Pipe.start dispatcher: Plumbing::EventDispatcher::Fiber.new limit: 8
|
167
|
+
@second_source = Plumbing::Pipe.start dispatcher: Plumbing::EventDispatcher::Fiber.new limit: 2
|
105
168
|
|
106
|
-
|
169
|
+
@junction = Plumbing::Junction.start @first_source, @second_source, dispatcher: Plumbing::EventDispatcher::Fiber.new
|
107
170
|
|
108
|
-
|
171
|
+
@filter = Plumbing::Filter.start source: @junction, dispatcher: Plumbing::EventDispatcher::Fibernew do |event|
|
172
|
+
%w[one-one two-two].include? event.type
|
173
|
+
end
|
109
174
|
|
110
|
-
|
175
|
+
Sync do
|
176
|
+
@first_source.notify "one-one"
|
177
|
+
@first_source.notify "one-two"
|
178
|
+
@second_source.notify "two-one"
|
179
|
+
@second_source.notify "two-two"
|
180
|
+
end
|
181
|
+
```
|
111
182
|
|
112
|
-
### Usage:
|
113
183
|
|
114
|
-
|
115
|
-
require "plumbing"
|
116
|
-
class BuildSequence < Plumbing::Chain
|
117
|
-
pre_condition :must_be_an_array do |input|
|
118
|
-
# you could replace this with a `validate` definition (using a Dry::Validation::Contract) if you prefer
|
119
|
-
input.is_a? Array
|
120
|
-
end
|
184
|
+
## Plumbing::RubberDuck - duck types and type-casts
|
121
185
|
|
122
|
-
|
123
|
-
# yes, this is a stupid post-condition but 🤷🏾♂️
|
124
|
-
output.length == 3
|
125
|
-
end
|
186
|
+
Define an [interface or protocol](https://en.wikipedia.org/wiki/Interface_(object-oriented_programming) specifying which messages you expect to be able to send. Then cast an object into that type, which first tests that the object can respond to those messages and limits you to sending those messages and no others.
|
126
187
|
|
127
|
-
perform :add_first
|
128
|
-
perform :add_second
|
129
|
-
perform :add_third
|
130
188
|
|
131
|
-
|
189
|
+
### Usage
|
132
190
|
|
133
|
-
|
134
|
-
input << "first"
|
135
|
-
end
|
191
|
+
Define your interface (Person in this example), then cast your objects (instances of PersonData and CarData).
|
136
192
|
|
137
|
-
|
138
|
-
|
139
|
-
end
|
193
|
+
```ruby
|
194
|
+
require "plumbing"
|
140
195
|
|
141
|
-
|
142
|
-
input << "third"
|
143
|
-
end
|
144
|
-
end
|
196
|
+
Person = Plumbing::RubberDuck.define :first_name, :last_name, :email
|
145
197
|
|
146
|
-
|
147
|
-
|
198
|
+
PersonData = Struct.new(:first_name, :last_name, :email, :favourite_food)
|
199
|
+
CarData = Struct.new(:make, :model, :colour)
|
148
200
|
|
149
|
-
|
150
|
-
|
201
|
+
@porsche_911 = CarData.new "Porsche", "911", "black"
|
202
|
+
@person = @porsche_911.as Person
|
203
|
+
# => Raises a TypeError
|
151
204
|
|
152
|
-
|
153
|
-
|
205
|
+
@alice = PersonData.new "Alice", "Aardvark", "alice@example.com", "Ice cream"
|
206
|
+
@person = @alice.as Person
|
207
|
+
@person.first_name
|
208
|
+
# => "Alice"
|
209
|
+
@person.email
|
210
|
+
# => "alice@example.com"
|
211
|
+
@person.favourite_food
|
212
|
+
# => NoMethodError - even though :favourite_food is a field in PersonData, it is not included in the definition of Person so cannot be accessed through the RubberDuck type
|
154
213
|
```
|
155
214
|
|
156
215
|
## Installation
|
@@ -167,7 +226,6 @@ Then:
|
|
167
226
|
require 'plumbing'
|
168
227
|
```
|
169
228
|
|
170
|
-
|
171
229
|
## Development
|
172
230
|
|
173
231
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
@@ -0,0 +1 @@
|
|
1
|
+
f8972f9d59a7a040262dd738514a53e38300604b524f67a16bfc465d69335838fd6861a429c164764f72a4172d54462c8816eeb43d4bafe8099c97eb9fe7b156
|
@@ -0,0 +1 @@
|
|
1
|
+
92f63ff4b002a27778854ba86d2bb4b60e4ce7eae93592d3c3e3f90aa03045d961b6dfea86901a7290485d4953df548c521ae49efd7d53547939208515aa392e
|
@@ -0,0 +1 @@
|
|
1
|
+
2309f738a6739650f259c456af69796e59c1de214579a8a274d56b7ccc8176e9e59cb0e63d744bd5a6ed40e93f4c57a368c3de2bc525163e90da9467a58c6387
|
data/lib/plumbing/error.rb
CHANGED
@@ -10,10 +10,4 @@ module Plumbing
|
|
10
10
|
|
11
11
|
# Error raised because an invalid [Event] object was pushed into the pipe
|
12
12
|
class InvalidEvent < Error; end
|
13
|
-
|
14
|
-
# Error raised because an invalid observer was registered
|
15
|
-
class InvalidObserver < Error; end
|
16
|
-
|
17
|
-
# Error raised because a BlockedPipe was used instead of an actual implementation of a Pipe
|
18
|
-
class PipeIsBlocked < Plumbing::Error; end
|
19
13
|
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.each 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,34 @@
|
|
1
|
+
module Plumbing
|
2
|
+
class EventDispatcher
|
3
|
+
def initialize observers: []
|
4
|
+
@observers = observers.as(Collection)
|
5
|
+
end
|
6
|
+
|
7
|
+
def add_observer observer = nil, &block
|
8
|
+
observer ||= block.to_proc
|
9
|
+
@observers << observer.as(Callable).target
|
10
|
+
observer
|
11
|
+
end
|
12
|
+
|
13
|
+
def remove_observer observer
|
14
|
+
@observers.delete observer
|
15
|
+
end
|
16
|
+
|
17
|
+
def is_observer? observer
|
18
|
+
@observers.include? observer
|
19
|
+
end
|
20
|
+
|
21
|
+
def dispatch event
|
22
|
+
@observers.each do |observer|
|
23
|
+
observer.call event
|
24
|
+
rescue => ex
|
25
|
+
puts ex
|
26
|
+
ex
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def shutdown
|
31
|
+
@observers = []
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/plumbing/filter.rb
CHANGED
@@ -1,19 +1,13 @@
|
|
1
1
|
module Plumbing
|
2
2
|
# A pipe that filters events from a source pipe
|
3
3
|
class Filter < Pipe
|
4
|
-
class InvalidFilter < Error; end
|
5
|
-
|
6
4
|
# Chain this pipe to the source pipe
|
7
5
|
# @param source [Plumbing::Pipe]
|
8
|
-
# @param accepts [
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
raise InvalidFilter.new "accepts and rejects must be arrays" unless accepts.is_a?(Array) && rejects.is_a?(Array)
|
14
|
-
@accepted_event_types = accepts
|
15
|
-
@rejected_event_types = rejects
|
16
|
-
source.add_observer do |event|
|
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
|
+
@accepts = accepts.as(Callable)
|
10
|
+
source.as(Observable).add_observer do |event|
|
17
11
|
filter_and_republish event
|
18
12
|
end
|
19
13
|
end
|
@@ -21,8 +15,7 @@ module Plumbing
|
|
21
15
|
private
|
22
16
|
|
23
17
|
def filter_and_republish event
|
24
|
-
return nil
|
25
|
-
return nil if @rejected_event_types.include? event.type
|
18
|
+
return nil unless @accepts.call event
|
26
19
|
dispatch event
|
27
20
|
end
|
28
21
|
end
|
@@ -0,0 +1,20 @@
|
|
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
|
+
source.as(Observable).add_observer do |event|
|
15
|
+
dispatch event
|
16
|
+
end
|
17
|
+
source
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/plumbing/pipe.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
module Plumbing
|
2
2
|
# A basic pipe
|
3
3
|
class Pipe
|
4
|
+
require_relative "event_dispatcher"
|
5
|
+
|
4
6
|
# Subclasses should call `super()` to ensure the pipe is initialised corrected
|
5
|
-
def initialize
|
6
|
-
@
|
7
|
+
def initialize dispatcher: nil
|
8
|
+
@dispatcher = dispatcher.nil? ? EventDispatcher.new : dispatcher.as(DispatchesEvents)
|
7
9
|
end
|
8
10
|
|
9
11
|
# Push an event into the pipe
|
@@ -27,38 +29,36 @@ module Plumbing
|
|
27
29
|
# @param &block [Block] (optional)
|
28
30
|
# @return an object representing this observer (dependent upon the implementation of the pipe itself)
|
29
31
|
# Either a `callable` or a `block` must be supplied. If the latter, it is converted to a [Proc]
|
30
|
-
def add_observer
|
31
|
-
observer
|
32
|
-
raise Plumbing::InvalidObserver.new "observer_does_not_respond_to_call" unless observer.respond_to? :call
|
33
|
-
@observers << observer
|
32
|
+
def add_observer(observer = nil, &)
|
33
|
+
@dispatcher.add_observer(observer, &)
|
34
34
|
end
|
35
35
|
|
36
36
|
# Remove an observer from this pipe
|
37
37
|
# @param observer
|
38
38
|
# This removes the given observer from this pipe. The observer should have previously been returned by #add_observer and is implementation-specific
|
39
39
|
def remove_observer observer
|
40
|
-
@
|
40
|
+
@dispatcher.remove_observer observer
|
41
41
|
end
|
42
42
|
|
43
43
|
# Test whether the given observer is observing this pipe
|
44
44
|
# @param observer
|
45
45
|
# @return [boolean]
|
46
46
|
def is_observer? observer
|
47
|
-
@
|
47
|
+
@dispatcher.is_observer? observer
|
48
48
|
end
|
49
49
|
|
50
50
|
# Close this pipe and perform any cleanup.
|
51
51
|
# Subclasses should override this to perform their own shutdown routines and call `super` to ensure everything is tidied up
|
52
52
|
def shutdown
|
53
53
|
# clean up and release any observers, just in case
|
54
|
-
@
|
54
|
+
@dispatcher.shutdown
|
55
55
|
end
|
56
56
|
|
57
57
|
# Start this pipe
|
58
58
|
# Subclasses may override this method to add any implementation specific details.
|
59
59
|
# By default any supplied parameters are called to the subclass' `initialize` method
|
60
|
-
def self.start(
|
61
|
-
new(
|
60
|
+
def self.start(*, **, &)
|
61
|
+
new(*, **, &)
|
62
62
|
end
|
63
63
|
|
64
64
|
protected
|
@@ -68,12 +68,7 @@ module Plumbing
|
|
68
68
|
# Enumerates all observers and `calls` them with this event
|
69
69
|
# Discards any errors raised by the observer so that all observers will be successfully notified
|
70
70
|
def dispatch event
|
71
|
-
@
|
72
|
-
observer.call event
|
73
|
-
rescue => ex
|
74
|
-
puts ex
|
75
|
-
ex
|
76
|
-
end
|
71
|
+
@dispatcher.dispatch event
|
77
72
|
end
|
78
73
|
end
|
79
74
|
end
|
@@ -1,5 +1,5 @@
|
|
1
1
|
module Plumbing
|
2
|
-
class
|
2
|
+
class Pipeline
|
3
3
|
module Contracts
|
4
4
|
def pre_condition name, &validator
|
5
5
|
pre_conditions[name.to_sym] = validator
|
@@ -25,19 +25,19 @@ module Plumbing
|
|
25
25
|
|
26
26
|
def validate_contract_for input
|
27
27
|
return true if @validation_contract.nil?
|
28
|
-
result = const_get(@validation_contract).new.call(input)
|
28
|
+
result = const_get(@validation_contract).new.as(Callable).call(input)
|
29
29
|
raise PreConditionError, result.errors.to_h.to_yaml unless result.success?
|
30
30
|
input
|
31
31
|
end
|
32
32
|
|
33
33
|
def validate_preconditions_for input
|
34
|
-
failed_preconditions = pre_conditions.select { |name, validator| !validator.call(input) }
|
34
|
+
failed_preconditions = pre_conditions.select { |name, validator| !validator.as(Callable).call(input) }
|
35
35
|
raise PreConditionError, failed_preconditions.keys.join(", ") if failed_preconditions.any?
|
36
36
|
input
|
37
37
|
end
|
38
38
|
|
39
39
|
def validate_postconditions_for output
|
40
|
-
failed_postconditions = post_conditions.select { |name, validator| !validator.call(output) }
|
40
|
+
failed_postconditions = post_conditions.select { |name, validator| !validator.as(Callable).call(output) }
|
41
41
|
raise PostConditionError, failed_postconditions.keys.join(", ") if failed_postconditions.any?
|
42
42
|
output
|
43
43
|
end
|
@@ -1,14 +1,8 @@
|
|
1
1
|
module Plumbing
|
2
|
-
class
|
2
|
+
class Pipeline
|
3
3
|
module Operations
|
4
|
-
def perform method, &implementation
|
5
|
-
|
6
|
-
operations << implementation
|
7
|
-
end
|
8
|
-
|
9
|
-
def embed method, class_name
|
10
|
-
implementation = ->(input, instance) { const_get(class_name).new.call(input) }
|
11
|
-
operations << implementation
|
4
|
+
def perform method, using: nil, &implementation
|
5
|
+
using.nil? ? perform_internal(method, &implementation) : perform_external(method, using)
|
12
6
|
end
|
13
7
|
|
14
8
|
def execute method
|
@@ -24,7 +18,7 @@ module Plumbing
|
|
24
18
|
validate_preconditions_for input
|
25
19
|
result = input
|
26
20
|
operations.each do |operation|
|
27
|
-
result = operation.call(result, instance)
|
21
|
+
result = operation.as(Callable).call(result, instance)
|
28
22
|
end
|
29
23
|
validate_postconditions_for result
|
30
24
|
result
|
@@ -35,6 +29,17 @@ module Plumbing
|
|
35
29
|
def operations
|
36
30
|
@operations ||= []
|
37
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.as(Callable).call(input) }
|
41
|
+
operations << implementation
|
42
|
+
end
|
38
43
|
end
|
39
44
|
end
|
40
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
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Plumbing
|
2
|
+
class RubberDuck
|
3
|
+
class Proxy
|
4
|
+
attr_reader :target
|
5
|
+
|
6
|
+
def initialize target, duck_type
|
7
|
+
@target = target
|
8
|
+
@duck_type = duck_type
|
9
|
+
end
|
10
|
+
|
11
|
+
def as duck_type
|
12
|
+
(duck_type == @duck_type) ? self : duck_type.proxy_for(target)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Plumbing
|
2
|
+
# A type-checker for duck-types
|
3
|
+
class RubberDuck
|
4
|
+
require_relative "rubber_duck/object"
|
5
|
+
require_relative "rubber_duck/proxy"
|
6
|
+
|
7
|
+
def initialize *methods
|
8
|
+
@methods = methods.map(&:to_sym)
|
9
|
+
@proxy_classes = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def verify object
|
13
|
+
missing_methods = @methods.reject { |method| object.respond_to? method }
|
14
|
+
raise TypeError, "Expected object to respond to #{missing_methods.join(", ")}" unless missing_methods.empty?
|
15
|
+
object
|
16
|
+
end
|
17
|
+
|
18
|
+
def proxy_for object
|
19
|
+
is_a_proxy?(object) || build_proxy_for(object)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.define *methods
|
23
|
+
new(*methods)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def is_a_proxy? object
|
29
|
+
@proxy_classes.value?(object.class) ? object : nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def build_proxy_for object
|
33
|
+
proxy_class_for(object).new(verify(object), self)
|
34
|
+
end
|
35
|
+
|
36
|
+
def proxy_class_for object
|
37
|
+
@proxy_classes[object.class] ||= define_proxy_class_for(object.class)
|
38
|
+
end
|
39
|
+
|
40
|
+
def define_proxy_class_for klass
|
41
|
+
Class.new(Plumbing::RubberDuck::Proxy).tap do |proxy_class|
|
42
|
+
@methods.each do |method|
|
43
|
+
proxy_class.define_method method do |*args, &block|
|
44
|
+
@target.send method, *args, &block
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,6 @@
|
|
1
|
+
module Plumbing
|
2
|
+
Callable = RubberDuck.define :call
|
3
|
+
Observable = RubberDuck.define :add_observer, :remove_observer, :is_observer?
|
4
|
+
DispatchesEvents = RubberDuck.define :add_observer, :remove_observer, :is_observer?, :shutdown, :dispatch
|
5
|
+
Collection = RubberDuck.define :each, :<<, :delete, :include?
|
6
|
+
end
|
data/lib/plumbing/version.rb
CHANGED
data/lib/plumbing.rb
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
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/chain"
|
10
|
-
|
11
3
|
module Plumbing
|
4
|
+
require_relative "plumbing/rubber_duck"
|
5
|
+
require_relative "plumbing/types"
|
6
|
+
require_relative "plumbing/error"
|
7
|
+
require_relative "plumbing/event"
|
8
|
+
require_relative "plumbing/pipe"
|
9
|
+
require_relative "plumbing/filter"
|
10
|
+
require_relative "plumbing/junction"
|
11
|
+
require_relative "plumbing/pipeline"
|
12
|
+
require_relative "plumbing/version"
|
12
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.2.
|
4
|
+
version: 0.2.2
|
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,15 +29,24 @@ 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
|
34
|
+
- checksums/standard-procedure-plumbing-0.2.2.gem.sha512
|
31
35
|
- lib/plumbing.rb
|
32
|
-
- lib/plumbing/chain.rb
|
33
|
-
- lib/plumbing/chain/contracts.rb
|
34
|
-
- lib/plumbing/chain/operations.rb
|
35
36
|
- lib/plumbing/error.rb
|
36
37
|
- lib/plumbing/event.rb
|
37
|
-
- lib/plumbing/
|
38
|
+
- lib/plumbing/event_dispatcher.rb
|
39
|
+
- lib/plumbing/event_dispatcher/fiber.rb
|
38
40
|
- lib/plumbing/filter.rb
|
41
|
+
- lib/plumbing/junction.rb
|
39
42
|
- lib/plumbing/pipe.rb
|
43
|
+
- lib/plumbing/pipeline.rb
|
44
|
+
- lib/plumbing/pipeline/contracts.rb
|
45
|
+
- lib/plumbing/pipeline/operations.rb
|
46
|
+
- lib/plumbing/rubber_duck.rb
|
47
|
+
- lib/plumbing/rubber_duck/object.rb
|
48
|
+
- lib/plumbing/rubber_duck/proxy.rb
|
49
|
+
- lib/plumbing/types.rb
|
40
50
|
- lib/plumbing/version.rb
|
41
51
|
- sig/plumbing.rbs
|
42
52
|
homepage: https://github.com/standard-procedure/plumbing
|
@@ -61,7 +71,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
61
71
|
- !ruby/object:Gem::Version
|
62
72
|
version: '0'
|
63
73
|
requirements: []
|
64
|
-
rubygems_version: 3.5.
|
74
|
+
rubygems_version: 3.5.17
|
65
75
|
signing_key:
|
66
76
|
specification_version: 4
|
67
77
|
summary: Plumbing - various pipelines for your ruby application
|
data/lib/plumbing/chain.rb
DELETED
@@ -1,14 +0,0 @@
|
|
1
|
-
require_relative "chain/contracts"
|
2
|
-
require_relative "chain/operations"
|
3
|
-
|
4
|
-
module Plumbing
|
5
|
-
# A chain of operations that are executed in sequence
|
6
|
-
class Chain
|
7
|
-
extend Plumbing::Chain::Contracts
|
8
|
-
extend Plumbing::Chain::Operations
|
9
|
-
|
10
|
-
def call input
|
11
|
-
self.class._call input, self
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|
data/lib/plumbing/fiber/pipe.rb
DELETED
@@ -1,36 +0,0 @@
|
|
1
|
-
require "async/task"
|
2
|
-
require "async/semaphore"
|
3
|
-
|
4
|
-
module Plumbing
|
5
|
-
module Fiber
|
6
|
-
# An implementation of a pipe that uses Fibers
|
7
|
-
class Pipe < Plumbing::Pipe
|
8
|
-
attr_reader :active
|
9
|
-
|
10
|
-
def initialize limit: 4
|
11
|
-
super()
|
12
|
-
@limit = 4
|
13
|
-
@semaphore = Async::Semaphore.new(@limit)
|
14
|
-
end
|
15
|
-
|
16
|
-
def << event
|
17
|
-
raise Plumbing::InvalidEvent.new "event is not a Plumbing::Event" unless event.is_a? Plumbing::Event
|
18
|
-
@semaphore.async do |task|
|
19
|
-
dispatch event, task
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
protected
|
24
|
-
|
25
|
-
def dispatch event, task
|
26
|
-
@observers.collect do |observer|
|
27
|
-
task.async do
|
28
|
-
observer.call event
|
29
|
-
rescue => ex
|
30
|
-
puts "Error: #{ex}"
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|