standard-procedure-plumbing 0.1.2 → 0.2.1

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: 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