standard-procedure-plumbing 0.2.0 → 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: 71e621c375dc0a17f928884eee2718fc7ed89a2f113374e90a5e6532914d71ea
4
- data.tar.gz: 6632624c924bdee49e9dc4b273da36792916c434d12b867d874bcd1ddb880b8b
3
+ metadata.gz: 00f89cef3ca3db2daf437446af1788755906434de2ec2d4e6e81130b26190f84
4
+ data.tar.gz: 44d27c6f52de7bbc24c488f741035d0a9fe4341036c423e51b5031896ca9f50a
5
5
  SHA512:
6
- metadata.gz: aa02b3cc5688ebeed1ec98f22142077d9c26e952bc34772d9395d6d120f2a5103efc67a5cc0e3f7508c937f6392a294f3d6088d8adca9d7415cf81cb2e7f28e6
7
- data.tar.gz: d2a164087e839fc3cd48d9b0c22cee9765541a388e4ffc48f5f614faa4690b69d7aa17a6bfc6d77d03ce2ed91e2af49cb8a02fa98e094117194414d7dbd49470
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,9 @@
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
+
1
7
  ## [0.2.0] - 2024-08-14
2
8
 
3
9
  - Added optional Dry::Validation support
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,64 +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
- 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.
105
-
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`.
107
-
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).
109
-
110
- You can also verify that the output generated is as expected by defining a `post_condition`.
111
-
112
- ### Usage:
113
-
158
+ Dispatching events asynchronously (using Fibers)
114
159
  ```ruby
115
160
  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
161
+ require "plumbing/event_dispatcher/fiber"
162
+ require "async"
121
163
 
122
- post_condition :must_have_three_elements do |output|
123
- # yes, this is a stupid post-condition but 🤷🏾‍♂️
124
- output.length == 3
125
- 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
126
167
 
127
- perform :add_first
128
- perform :add_second
129
- perform :add_third
130
-
131
- private
168
+ @junction = Plumbing::Junction.start @first_source, @second_source, dispatcher: Plumbing::EventDispatcher::Fiber.new
132
169
 
133
- def add_first input
134
- input << "first"
135
- end
136
-
137
- def add_second input
138
- input << "second"
139
- end
140
-
141
- def add_third input
142
- input << "third"
143
- end
170
+ @filter = Plumbing::Filter.start source: @junction, dispatcher: Plumbing::EventDispatcher::Fibernew do |event|
171
+ %w[one-one two-two].include? event.type
144
172
  end
145
173
 
146
- BuildSequence.new.call []
147
- # => ["first", "second", "third"]
148
-
149
- BuildSequence.new.call 1
150
- # => Plumbing::PreconditionError("must_be_an_array")
151
-
152
- BuildSequence.new.call ["extra element"]
153
- # => 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
154
180
  ```
155
181
 
182
+
156
183
  ## Installation
157
184
 
158
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,18 +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
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
16
11
  source.add_observer do |event|
17
12
  filter_and_republish event
18
13
  end
@@ -21,8 +16,7 @@ module Plumbing
21
16
  private
22
17
 
23
18
  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
19
+ return nil unless @accepts.call event
26
20
  dispatch event
27
21
  end
28
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,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 || EventDispatcher.new
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
@@ -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
@@ -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.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
@@ -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.1"
5
5
  end
data/lib/plumbing.rb CHANGED
@@ -6,7 +6,8 @@ require_relative "plumbing/error"
6
6
  require_relative "plumbing/event"
7
7
  require_relative "plumbing/pipe"
8
8
  require_relative "plumbing/filter"
9
- require_relative "plumbing/chain"
9
+ require_relative "plumbing/junction"
10
+ require_relative "plumbing/pipeline"
10
11
 
11
12
  module Plumbing
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.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,15 +29,19 @@ 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/chain.rb
33
- - lib/plumbing/chain/contracts.rb
34
- - lib/plumbing/chain/operations.rb
35
35
  - lib/plumbing/error.rb
36
36
  - lib/plumbing/event.rb
37
- - lib/plumbing/fiber/pipe.rb
37
+ - lib/plumbing/event_dispatcher.rb
38
+ - lib/plumbing/event_dispatcher/fiber.rb
38
39
  - lib/plumbing/filter.rb
40
+ - lib/plumbing/junction.rb
39
41
  - lib/plumbing/pipe.rb
42
+ - lib/plumbing/pipeline.rb
43
+ - lib/plumbing/pipeline/contracts.rb
44
+ - lib/plumbing/pipeline/operations.rb
40
45
  - lib/plumbing/version.rb
41
46
  - sig/plumbing.rbs
42
47
  homepage: https://github.com/standard-procedure/plumbing
@@ -61,7 +66,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
61
66
  - !ruby/object:Gem::Version
62
67
  version: '0'
63
68
  requirements: []
64
- rubygems_version: 3.5.9
69
+ rubygems_version: 3.5.17
65
70
  signing_key:
66
71
  specification_version: 4
67
72
  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