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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a0bfd8678cd958f92a30d6c1879e664ac790637b400a2fd73302a7ed60a3b450
4
- data.tar.gz: f06824216f1d2da14fa645b0f246106180e6e8e69f8fa7d5526e8a343ac77923
3
+ metadata.gz: 00f89cef3ca3db2daf437446af1788755906434de2ec2d4e6e81130b26190f84
4
+ data.tar.gz: 44d27c6f52de7bbc24c488f741035d0a9fe4341036c423e51b5031896ca9f50a
5
5
  SHA512:
6
- metadata.gz: ac2e9872aba1ecb6c4f2831ee6da3689afdcf1162c5bc0b65eaa2298ae1a00f4d698fb7909c34e10daa6b14cd10bcd8ab54cd92839fab7a1107cd93de831c2c5
7
- data.tar.gz: 64f585e329a112504b692b788d6b6c9562bc188eb9c621a33001f2aed69d92c6260909c8a3df174436bfeda345791b7ca370afb09fdad2d281d466efcb0784fc
6
+ metadata.gz: 6785c35e5596df5718a8ce458ced0713abbe3d18516a90b6353c806da8f867d3bf4d48d70b8b31b15b7c23a491fa2c855c091250aa96bdf97595f813b53e8e18
7
+ data.tar.gz: 22169d08af47f789dd5a950635ad46598c6a77f402a2221ed719f369df00b065185b402f14ba2dd2dd82b54cac8774d67d1adc2c07524307a644be11707d7956
@@ -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,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, accepts: %w[important urgent]
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
- @join = Plumbing::Junction.start @first_source, @second_source
146
+ @junction = Plumbing::Junction.start @first_source, @second_source
87
147
 
88
- @observer = @join.add_observer do |event|
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
- ## 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.
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
- class BuildSequence < Plumbing::Chain
109
- pre_condition :must_be_an_array do |input|
110
- input.is_a? Array
111
- end
161
+ require "plumbing/event_dispatcher/fiber"
162
+ require "async"
112
163
 
113
- post_condition :must_have_three_elements do |output|
114
- # yes, this is a stupid post-condition but it shows how you can ensure your outputs are valid
115
- output.length == 3
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
- perform :add_first
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
- def add_first input
125
- input << "first"
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
- BuildSequence.new.call []
138
- # => ["first", "second", "third"]
139
-
140
- BuildSequence.new.call 1
141
- # => Plumbing::PreconditionError("must_be_an_array")
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
@@ -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 BlockedPipe was used instead of an actual implementation of a Pipe
18
- class PipeIsBlocked < Plumbing::Error; end
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
@@ -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 < BlockedPipe
6
- class InvalidFilter < Error; end
7
-
3
+ class Filter < Pipe
8
4
  # Chain this pipe to the source pipe
9
- # @param source [Plumbing::BlockedPipe]
10
- # @param accepts [Array[String]] event types that this filter will allow through (or pass [] to allow all)
11
- # @param rejects [Array[String]] event types that this filter will not allow through
12
- def initialize source:, accepts: [], rejects: []
13
- super()
14
- raise InvalidFilter.new "source must be a Plumbing::BlockedPipe descendant" unless source.is_a? Plumbing::BlockedPipe
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(event)
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
- raise InvalidEvent.new "event is not a Plumbing::Event" unless event.is_a? Plumbing::Event
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
- # An implementation of a pipe that uses Fibers
5
- class Pipe < BlockedPipe
6
- def initialize
7
- super
8
- @fiber = Fiber.new do |initial_event|
9
- start_run_loop initial_event
10
- end
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 "event is not a Plumbing::Event" unless event.is_a? Plumbing::Event
15
- @fiber.resume event
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
- super
20
- @fiber.resume :shutdown
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
- def get_next_event
26
- Fiber.yield
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
- # A chain of operations that are executed in sequence
3
- class Chain
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 perform method, &implementation
14
- implementation ||= ->(params, instance) { instance.send(method, params) }
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Plumbing
4
- VERSION = "0.1.2"
4
+ VERSION = "0.2.1"
5
5
  end
data/lib/plumbing.rb CHANGED
@@ -1,10 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Plumbing
4
- require_relative "plumbing/version"
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
- require_relative "plumbing/error"
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.2
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-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,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://theartandscienceofruby.com
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://theartandscienceofruby.com
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.9
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