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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 71e621c375dc0a17f928884eee2718fc7ed89a2f113374e90a5e6532914d71ea
4
- data.tar.gz: 6632624c924bdee49e9dc4b273da36792916c434d12b867d874bcd1ddb880b8b
3
+ metadata.gz: 6074870313ece34eb4b9565602db4de70bb5b7e14e8a9051d85bb46b6fc64bf5
4
+ data.tar.gz: a81292b0ad9e87dfcce61d531e891c1b4ed25ccda3331ceb731c082c6e9c7c16
5
5
  SHA512:
6
- metadata.gz: aa02b3cc5688ebeed1ec98f22142077d9c26e952bc34772d9395d6d120f2a5103efc67a5cc0e3f7508c937f6392a294f3d6088d8adca9d7415cf81cb2e7f28e6
7
- data.tar.gz: d2a164087e839fc3cd48d9b0c22cee9765541a388e4ffc48f5f614faa4690b69d7aa17a6bfc6d77d03ce2ed91e2af49cb8a02fa98e094117194414d7dbd49470
6
+ metadata.gz: 45827e921f7bc272e0688a6477d405a81e8c9ea64405078782fbf5aa45658066bf7f9c5503692f4f6eadfdbb08d21221eb4e0b0731654e4808e0f7ec1111f9bb
7
+ data.tar.gz: 1ba319545accf7051393845b1883d5eb69aeacba182c86f26ca6e95e7761711e922904d2ec5427872e8dcd6ccdf5fba25737fe5c20a887cc06ba23ab2184c81a
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": "2.0.0",
3
+ "tasks": [
4
+ {
5
+ "label": "rspec",
6
+ "type": "shell",
7
+ "command": "rspec",
8
+ "problemMatcher": []
9
+ }
10
+ ]
11
+ }
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, accepts: %w[important urgent]
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
- @join = Plumbing::Junction.start @first_source, @second_source
147
+ @junction = Plumbing::Junction.start @first_source, @second_source
87
148
 
88
- @observer = @join.add_observer do |event|
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
- ## Plumbing::Chain - a chain of operations that occur in sequence
99
-
100
- Define a sequence of operations that proceed in order, passing their output from one operation as the input to another.
159
+ Dispatching events asynchronously (using Fibers)
160
+ ```ruby
161
+ require "plumbing"
162
+ require "plumbing/event_dispatcher/fiber"
163
+ require "async"
101
164
 
102
- Use `perform` to define a step that takes some input and returns a different output.
103
- Use `execute` to define a step that takes some input and returns that same input.
104
- Use `embed` to define a step that uses another `Plumbing::Chain` class to generate the output.
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
- 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`.
169
+ @junction = Plumbing::Junction.start @first_source, @second_source, dispatcher: Plumbing::EventDispatcher::Fiber.new
107
170
 
108
- If you don't want to use dry-validation, you can instead define a `pre_condition` - there's nothing to stop you defining a contract as well as pre_conditions (with the contract being verified first).
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
- You can also verify that the output generated is as expected by defining a `post_condition`.
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
- ```ruby
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
- post_condition :must_have_three_elements do |output|
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
- private
189
+ ### Usage
132
190
 
133
- def add_first input
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
- def add_second input
138
- input << "second"
139
- end
193
+ ```ruby
194
+ require "plumbing"
140
195
 
141
- def add_third input
142
- input << "third"
143
- end
144
- end
196
+ Person = Plumbing::RubberDuck.define :first_name, :last_name, :email
145
197
 
146
- BuildSequence.new.call []
147
- # => ["first", "second", "third"]
198
+ PersonData = Struct.new(:first_name, :last_name, :email, :favourite_food)
199
+ CarData = Struct.new(:make, :model, :colour)
148
200
 
149
- BuildSequence.new.call 1
150
- # => Plumbing::PreconditionError("must_be_an_array")
201
+ @porsche_911 = CarData.new "Porsche", "911", "black"
202
+ @person = @porsche_911.as Person
203
+ # => Raises a TypeError
151
204
 
152
- BuildSequence.new.call ["extra element"]
153
- # => Plumbing::PostconditionError("must_have_three_elements")
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
@@ -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
@@ -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 [Array[String]] event types that this filter will allow through (or pass [] to allow all)
9
- # @param rejects [Array[String]] event types that this filter will not allow through
10
- def initialize source:, accepts: [], rejects: []
11
- super()
12
- raise InvalidFilter.new "source must be a Plumbing::Pipe descendant" unless source.is_a? Plumbing::Pipe
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 if @accepted_event_types.any? && !@accepted_event_types.include?(event.type)
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
- @observers = []
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 observer = nil, &block
31
- observer ||= block.to_proc
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
- @observers.delete observer
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
- @observers.include? observer
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
- @observers = []
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(**params)
61
- new(**params)
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
- @observers.collect do |observer|
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 Chain
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 Chain
2
+ class Pipeline
3
3
  module Operations
4
- def perform method, &implementation
5
- implementation ||= ->(input, instance) { instance.send(method, input) }
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,9 @@
1
+ module Plumbing
2
+ class RubberDuck
3
+ ::Object.class_eval do
4
+ def as duck_type
5
+ duck_type.proxy_for self
6
+ end
7
+ end
8
+ end
9
+ 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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Plumbing
4
- VERSION = "0.2.0"
4
+ VERSION = "0.2.2"
5
5
  end
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.0
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-14 00:00:00.000000000 Z
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/fiber/pipe.rb
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.9
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
@@ -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
@@ -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