standard-procedure-plumbing 0.2.0 → 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: 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