standard-procedure-plumbing 0.1.1 → 0.2.0
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 +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +11 -4
- data/checksums/standard-procedure-plumbing-0.1.2.gem.sha512 +1 -0
- data/lib/plumbing/chain/contracts.rb +46 -0
- data/lib/plumbing/chain/operations.rb +40 -0
- data/lib/plumbing/chain.rb +7 -52
- data/lib/plumbing/error.rb +2 -2
- data/lib/plumbing/event.rb +1 -13
- data/lib/plumbing/fiber/pipe.rb +36 -0
- data/lib/plumbing/filter.rb +9 -14
- data/lib/plumbing/pipe.rb +64 -13
- data/lib/plumbing/version.rb +1 -1
- data/lib/plumbing.rb +8 -7
- metadata +12 -39
- data/Guardfile +0 -32
- data/lib/plumbing/blocked_pipe.rb +0 -115
- data/lib/plumbing/concurrent/pipe.rb +0 -72
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 71e621c375dc0a17f928884eee2718fc7ed89a2f113374e90a5e6532914d71ea
|
4
|
+
data.tar.gz: 6632624c924bdee49e9dc4b273da36792916c434d12b867d874bcd1ddb880b8b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: aa02b3cc5688ebeed1ec98f22142077d9c26e952bc34772d9395d6d120f2a5103efc67a5cc0e3f7508c937f6392a294f3d6088d8adca9d7415cf81cb2e7f28e6
|
7
|
+
data.tar.gz: d2a164087e839fc3cd48d9b0c22cee9765541a388e4ffc48f5f614faa4690b69d7aa17a6bfc6d77d03ce2ed91e2af49cb8a02fa98e094117194414d7dbd49470
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,13 @@
|
|
1
|
+
## [0.2.0] - 2024-08-14
|
2
|
+
|
3
|
+
- Added optional Dry::Validation support
|
4
|
+
- Use Async for fiber-based pipes
|
5
|
+
|
6
|
+
## [0.1.2] - 2024-08-14
|
7
|
+
|
8
|
+
- Removed dependencies
|
9
|
+
- Removed Ractor-based concurrent pipe (as I don't trust it yet)
|
10
|
+
|
1
11
|
## [0.1.1] - 2024-08-14
|
2
12
|
|
3
13
|
- Tidied up the code
|
data/README.md
CHANGED
@@ -95,13 +95,19 @@ end
|
|
95
95
|
# => "two"
|
96
96
|
```
|
97
97
|
|
98
|
-
There is also [Plumbing::Concurrent::Pipe](/lib/plumbing/concurrent/pipe.rb) that uses a Ractor to dispatch events. Use with caution as Ractors are still experimental, plus they have strict conditions about the data that can be passed across Ractor boundaries.
|
99
|
-
|
100
98
|
## Plumbing::Chain - a chain of operations that occur in sequence
|
101
99
|
|
102
100
|
Define a sequence of operations that proceed in order, passing their output from one operation as the input to another.
|
103
101
|
|
104
|
-
|
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`.
|
105
111
|
|
106
112
|
### Usage:
|
107
113
|
|
@@ -109,11 +115,12 @@ You can define pre-conditions (which validate the inputs supplied) or post-condi
|
|
109
115
|
require "plumbing"
|
110
116
|
class BuildSequence < Plumbing::Chain
|
111
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
|
112
119
|
input.is_a? Array
|
113
120
|
end
|
114
121
|
|
115
122
|
post_condition :must_have_three_elements do |output|
|
116
|
-
# yes, this is a stupid post-condition but
|
123
|
+
# yes, this is a stupid post-condition but 🤷🏾♂️
|
117
124
|
output.length == 3
|
118
125
|
end
|
119
126
|
|
@@ -0,0 +1 @@
|
|
1
|
+
18d6d3138d2879c1672ab60b1534a6bc382e99a67adb4f8eff9c60ffb96b78b148bffac54ff1f7e78a2da1fc36d000041806b3a73993416c62279ef3ca09beba
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Plumbing
|
2
|
+
class Chain
|
3
|
+
module Contracts
|
4
|
+
def pre_condition name, &validator
|
5
|
+
pre_conditions[name.to_sym] = validator
|
6
|
+
end
|
7
|
+
|
8
|
+
def validate_with contract_class
|
9
|
+
@validation_contract = contract_class
|
10
|
+
end
|
11
|
+
|
12
|
+
def post_condition name, &validator
|
13
|
+
post_conditions[name.to_sym] = validator
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def pre_conditions
|
19
|
+
@pre_conditions ||= {}
|
20
|
+
end
|
21
|
+
|
22
|
+
def post_conditions
|
23
|
+
@post_conditions ||= {}
|
24
|
+
end
|
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
|
+
|
33
|
+
def validate_preconditions_for input
|
34
|
+
failed_preconditions = pre_conditions.select { |name, validator| !validator.call(input) }
|
35
|
+
raise PreConditionError, failed_preconditions.keys.join(", ") if failed_preconditions.any?
|
36
|
+
input
|
37
|
+
end
|
38
|
+
|
39
|
+
def validate_postconditions_for output
|
40
|
+
failed_postconditions = post_conditions.select { |name, validator| !validator.call(output) }
|
41
|
+
raise PostConditionError, failed_postconditions.keys.join(", ") if failed_postconditions.any?
|
42
|
+
output
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Plumbing
|
2
|
+
class Chain
|
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
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute method
|
15
|
+
implementation ||= ->(input, instance) do
|
16
|
+
instance.send(method, input)
|
17
|
+
input
|
18
|
+
end
|
19
|
+
operations << implementation
|
20
|
+
end
|
21
|
+
|
22
|
+
def _call input, instance
|
23
|
+
validate_contract_for input
|
24
|
+
validate_preconditions_for input
|
25
|
+
result = input
|
26
|
+
operations.each do |operation|
|
27
|
+
result = operation.call(result, instance)
|
28
|
+
end
|
29
|
+
validate_postconditions_for result
|
30
|
+
result
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def operations
|
36
|
+
@operations ||= []
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/plumbing/chain.rb
CHANGED
@@ -1,59 +1,14 @@
|
|
1
|
+
require_relative "chain/contracts"
|
2
|
+
require_relative "chain/operations"
|
3
|
+
|
1
4
|
module Plumbing
|
2
5
|
# A chain of operations that are executed in sequence
|
3
6
|
class Chain
|
4
|
-
|
5
|
-
|
6
|
-
end
|
7
|
-
|
8
|
-
class << self
|
9
|
-
def pre_condition name, &validator
|
10
|
-
pre_conditions[name.to_sym] = validator
|
11
|
-
end
|
12
|
-
|
13
|
-
def perform method, &implementation
|
14
|
-
implementation ||= ->(params, instance) { instance.send(method, params) }
|
15
|
-
operations << implementation
|
16
|
-
end
|
17
|
-
|
18
|
-
def post_condition name, &validator
|
19
|
-
post_conditions[name.to_sym] = validator
|
20
|
-
end
|
21
|
-
|
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
|
-
private
|
33
|
-
|
34
|
-
def operations
|
35
|
-
@operations ||= []
|
36
|
-
end
|
37
|
-
|
38
|
-
def pre_conditions
|
39
|
-
@pre_conditions ||= {}
|
40
|
-
end
|
41
|
-
|
42
|
-
def post_conditions
|
43
|
-
@post_conditions ||= {}
|
44
|
-
end
|
45
|
-
|
46
|
-
def validate_preconditions_for input
|
47
|
-
failed_preconditions = pre_conditions.select { |name, validator| !validator.call(input) }
|
48
|
-
raise PreConditionError, failed_preconditions.keys.join(", ") if failed_preconditions.any?
|
49
|
-
input
|
50
|
-
end
|
7
|
+
extend Plumbing::Chain::Contracts
|
8
|
+
extend Plumbing::Chain::Operations
|
51
9
|
|
52
|
-
|
53
|
-
|
54
|
-
raise PostConditionError, failed_postconditions.keys.join(", ") if failed_postconditions.any?
|
55
|
-
output
|
56
|
-
end
|
10
|
+
def call input
|
11
|
+
self.class._call input, self
|
57
12
|
end
|
58
13
|
end
|
59
14
|
end
|
data/lib/plumbing/error.rb
CHANGED
@@ -9,10 +9,10 @@ module Plumbing
|
|
9
9
|
class PostConditionError < Error; end
|
10
10
|
|
11
11
|
# Error raised because an invalid [Event] object was pushed into the pipe
|
12
|
-
InvalidEvent
|
12
|
+
class InvalidEvent < Error; end
|
13
13
|
|
14
14
|
# Error raised because an invalid observer was registered
|
15
|
-
InvalidObserver
|
15
|
+
class InvalidObserver < Error; end
|
16
16
|
|
17
17
|
# Error raised because a BlockedPipe was used instead of an actual implementation of a Pipe
|
18
18
|
class PipeIsBlocked < Plumbing::Error; end
|
data/lib/plumbing/event.rb
CHANGED
@@ -1,17 +1,5 @@
|
|
1
|
-
require "dry/types"
|
2
|
-
require "dry/struct"
|
3
|
-
|
4
1
|
module Plumbing
|
5
2
|
# An immutable data structure representing an Event
|
6
|
-
|
7
|
-
module Types
|
8
|
-
include Dry::Types()
|
9
|
-
SequenceNumber = Strict::Integer
|
10
|
-
Type = Strict::String
|
11
|
-
Data = Strict::Hash.map(Coercible::Symbol, Nominal::Any).default({}.freeze)
|
12
|
-
end
|
13
|
-
|
14
|
-
attribute :type, Types::Type
|
15
|
-
attribute :data, Types::Data
|
3
|
+
Event = Data.define :type, :data do
|
16
4
|
end
|
17
5
|
end
|
@@ -0,0 +1,36 @@
|
|
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
|
data/lib/plumbing/filter.rb
CHANGED
@@ -1,25 +1,20 @@
|
|
1
|
-
require "dry/types"
|
2
|
-
require_relative "blocked_pipe"
|
3
|
-
|
4
1
|
module Plumbing
|
5
2
|
# A pipe that filters events from a source pipe
|
6
|
-
class Filter <
|
7
|
-
|
8
|
-
include Dry::Types()
|
9
|
-
Source = Instance(Plumbing::BlockedPipe)
|
10
|
-
EventTypes = Array.of(Plumbing::Event::Types::Type)
|
11
|
-
end
|
3
|
+
class Filter < Pipe
|
4
|
+
class InvalidFilter < Error; end
|
12
5
|
|
13
6
|
# Chain this pipe to the source pipe
|
14
|
-
# @param source [Plumbing::
|
7
|
+
# @param source [Plumbing::Pipe]
|
15
8
|
# @param accepts [Array[String]] event types that this filter will allow through (or pass [] to allow all)
|
16
9
|
# @param rejects [Array[String]] event types that this filter will not allow through
|
17
10
|
def initialize source:, accepts: [], rejects: []
|
18
11
|
super()
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
12
|
+
raise InvalidFilter.new "source must be a Plumbing::Pipe descendant" unless source.is_a? Plumbing::Pipe
|
13
|
+
raise InvalidFilter.new "accepts and rejects must be arrays" unless accepts.is_a?(Array) && rejects.is_a?(Array)
|
14
|
+
@accepted_event_types = accepts
|
15
|
+
@rejected_event_types = rejects
|
16
|
+
source.add_observer do |event|
|
17
|
+
filter_and_republish event
|
23
18
|
end
|
24
19
|
end
|
25
20
|
|
data/lib/plumbing/pipe.rb
CHANGED
@@ -1,28 +1,79 @@
|
|
1
|
-
require_relative "blocked_pipe"
|
2
|
-
|
3
1
|
module Plumbing
|
4
|
-
#
|
5
|
-
class Pipe
|
2
|
+
# A basic pipe
|
3
|
+
class Pipe
|
4
|
+
# Subclasses should call `super()` to ensure the pipe is initialised corrected
|
6
5
|
def initialize
|
7
|
-
|
8
|
-
@fiber = Fiber.new do |initial_event|
|
9
|
-
start_run_loop initial_event
|
10
|
-
end
|
6
|
+
@observers = []
|
11
7
|
end
|
12
8
|
|
9
|
+
# Push an event into the pipe
|
10
|
+
# @param event [Plumbing::Event] the event to push into the pipe
|
13
11
|
def << event
|
14
|
-
|
12
|
+
raise Plumbing::InvalidEvent.new event unless event.is_a? Plumbing::Event
|
13
|
+
dispatch event
|
14
|
+
end
|
15
|
+
|
16
|
+
# A shortcut to creating and then pushing an event
|
17
|
+
# @param event_type [String] representing the type of event this is
|
18
|
+
# @param data [Hash] representing the event-specific data to be passed to the observers
|
19
|
+
def notify event_type, data = nil
|
20
|
+
Event.new(type: event_type, data: data).tap do |event|
|
21
|
+
self << event
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Add an observer to this pipe
|
26
|
+
# @param callable [Proc] (optional)
|
27
|
+
# @param &block [Block] (optional)
|
28
|
+
# @return an object representing this observer (dependent upon the implementation of the pipe itself)
|
29
|
+
# 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
|
15
34
|
end
|
16
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
|
+
@observers.delete observer
|
41
|
+
end
|
42
|
+
|
43
|
+
# Test whether the given observer is observing this pipe
|
44
|
+
# @param observer
|
45
|
+
# @return [boolean]
|
46
|
+
def is_observer? observer
|
47
|
+
@observers.include? 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
|
17
52
|
def shutdown
|
18
|
-
|
19
|
-
@
|
53
|
+
# clean up and release any observers, just in case
|
54
|
+
@observers = []
|
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(**params)
|
61
|
+
new(**params)
|
20
62
|
end
|
21
63
|
|
22
64
|
protected
|
23
65
|
|
24
|
-
|
25
|
-
|
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
|
+
@observers.collect do |observer|
|
72
|
+
observer.call event
|
73
|
+
rescue => ex
|
74
|
+
puts ex
|
75
|
+
ex
|
76
|
+
end
|
26
77
|
end
|
27
78
|
end
|
28
79
|
end
|
data/lib/plumbing/version.rb
CHANGED
data/lib/plumbing.rb
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
require_relative "plumbing/version"
|
4
|
+
|
5
|
+
require_relative "plumbing/error"
|
6
|
+
require_relative "plumbing/event"
|
7
|
+
require_relative "plumbing/pipe"
|
8
|
+
require_relative "plumbing/filter"
|
9
|
+
require_relative "plumbing/chain"
|
6
10
|
|
7
|
-
|
8
|
-
require_relative "plumbing/event"
|
9
|
-
require_relative "plumbing/pipe"
|
10
|
-
require_relative "plumbing/chain"
|
11
|
+
module Plumbing
|
11
12
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: standard-procedure-plumbing
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rahoul Baruah
|
@@ -9,36 +9,8 @@ autorequire:
|
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
11
|
date: 2024-08-14 00:00:00.000000000 Z
|
12
|
-
dependencies:
|
13
|
-
|
14
|
-
name: dry-types
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
16
|
-
requirements:
|
17
|
-
- - ">="
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: '0'
|
20
|
-
type: :runtime
|
21
|
-
prerelease: false
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
23
|
-
requirements:
|
24
|
-
- - ">="
|
25
|
-
- !ruby/object:Gem::Version
|
26
|
-
version: '0'
|
27
|
-
- !ruby/object:Gem::Dependency
|
28
|
-
name: dry-struct
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
30
|
-
requirements:
|
31
|
-
- - ">="
|
32
|
-
- !ruby/object:Gem::Version
|
33
|
-
version: '0'
|
34
|
-
type: :runtime
|
35
|
-
prerelease: false
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
37
|
-
requirements:
|
38
|
-
- - ">="
|
39
|
-
- !ruby/object:Gem::Version
|
40
|
-
version: '0'
|
41
|
-
description: An event pipeline
|
12
|
+
dependencies: []
|
13
|
+
description: A composable event pipeline and sequential pipelines of operations
|
42
14
|
email:
|
43
15
|
- rahoulb@echodek.co
|
44
16
|
executables: []
|
@@ -51,28 +23,29 @@ files:
|
|
51
23
|
- ".standard.yml"
|
52
24
|
- CHANGELOG.md
|
53
25
|
- CODE_OF_CONDUCT.md
|
54
|
-
- Guardfile
|
55
26
|
- LICENSE
|
56
27
|
- README.md
|
57
28
|
- Rakefile
|
58
29
|
- checksums/standard-procedure-plumbing-0.1.1.gem.sha512
|
30
|
+
- checksums/standard-procedure-plumbing-0.1.2.gem.sha512
|
59
31
|
- lib/plumbing.rb
|
60
|
-
- lib/plumbing/blocked_pipe.rb
|
61
32
|
- lib/plumbing/chain.rb
|
62
|
-
- lib/plumbing/
|
33
|
+
- lib/plumbing/chain/contracts.rb
|
34
|
+
- lib/plumbing/chain/operations.rb
|
63
35
|
- lib/plumbing/error.rb
|
64
36
|
- lib/plumbing/event.rb
|
37
|
+
- lib/plumbing/fiber/pipe.rb
|
65
38
|
- lib/plumbing/filter.rb
|
66
39
|
- lib/plumbing/pipe.rb
|
67
40
|
- lib/plumbing/version.rb
|
68
41
|
- sig/plumbing.rbs
|
69
|
-
homepage: https://
|
42
|
+
homepage: https://github.com/standard-procedure/plumbing
|
70
43
|
licenses: []
|
71
44
|
metadata:
|
72
45
|
allowed_push_host: https://rubygems.org
|
73
|
-
homepage_uri: https://
|
74
|
-
source_code_uri: https://github.com
|
75
|
-
changelog_uri: https://github.com
|
46
|
+
homepage_uri: https://github.com/standard-procedure/plumbing
|
47
|
+
source_code_uri: https://github.com/standard-procedure/plumbing
|
48
|
+
changelog_uri: https://github.com/standard-procedure/plumbing/blob/main/CHANGELOG.md
|
76
49
|
post_install_message:
|
77
50
|
rdoc_options: []
|
78
51
|
require_paths:
|
@@ -91,5 +64,5 @@ requirements: []
|
|
91
64
|
rubygems_version: 3.5.9
|
92
65
|
signing_key:
|
93
66
|
specification_version: 4
|
94
|
-
summary:
|
67
|
+
summary: Plumbing - various pipelines for your ruby application
|
95
68
|
test_files: []
|
data/Guardfile
DELETED
@@ -1,32 +0,0 @@
|
|
1
|
-
ignore(/bin/, /log/, /public/, /storage/, /tmp/)
|
2
|
-
|
3
|
-
group :formatting do
|
4
|
-
guard :standardrb, fix: true, all_on_start: true, progress: true do
|
5
|
-
watch(/.+\.rb$/)
|
6
|
-
watch(/.+\.thor$/)
|
7
|
-
watch(/.+\.rake$/)
|
8
|
-
watch(/Guardfile$/)
|
9
|
-
watch(/Rakefile$/)
|
10
|
-
watch(/Gemfile$/)
|
11
|
-
end
|
12
|
-
end
|
13
|
-
|
14
|
-
group :development do
|
15
|
-
guard :rspec, cmd: "bundle exec rspec" do
|
16
|
-
watch("spec/.+_helper.rb") { "spec" }
|
17
|
-
watch(%r{^spec/.+_spec\.rb$})
|
18
|
-
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
19
|
-
end
|
20
|
-
|
21
|
-
guard :bundler do
|
22
|
-
require "guard/bundler"
|
23
|
-
require "guard/bundler/verify"
|
24
|
-
helper = Guard::Bundler::Verify.new
|
25
|
-
|
26
|
-
files = ["Gemfile"]
|
27
|
-
files += Dir["*.gemspec"] if files.any? { |f| helper.uses_gemspec?(f) }
|
28
|
-
|
29
|
-
# Assume files are symlinked from somewhere
|
30
|
-
files.each { |file| watch(helper.real_path(file)) }
|
31
|
-
end
|
32
|
-
end
|
@@ -1,115 +0,0 @@
|
|
1
|
-
require "dry/types"
|
2
|
-
require_relative "error"
|
3
|
-
require_relative "event"
|
4
|
-
|
5
|
-
module Plumbing
|
6
|
-
# The "plumbing" for a Pipe.
|
7
|
-
# This class is "blocked", in that it won't push any events to registered observers.
|
8
|
-
# Instead, this is the basis for subclasses like [Plumbing::Pipe] which actually allow events to flow through them.
|
9
|
-
class BlockedPipe
|
10
|
-
module Types
|
11
|
-
include Dry::Types()
|
12
|
-
# Events must be Plumbing::Event instances or subclasses
|
13
|
-
Event = Instance(Plumbing::Event)
|
14
|
-
# Observers must have a `call` method
|
15
|
-
Observer = Interface(:call)
|
16
|
-
end
|
17
|
-
|
18
|
-
# Create a new BlockedPipe
|
19
|
-
# Subclasses should call `super()` to ensure the pipe is initialised corrected
|
20
|
-
def initialize
|
21
|
-
@observers = []
|
22
|
-
end
|
23
|
-
|
24
|
-
# Push an event into the pipe
|
25
|
-
# @param event [Plumbing::Event] the event to push into the pipe
|
26
|
-
# Subclasses should implement this method
|
27
|
-
def << event
|
28
|
-
raise PipeIsBlocked
|
29
|
-
end
|
30
|
-
|
31
|
-
# A shortcut to creating and then pushing an event
|
32
|
-
# @param event_type [String] representing the type of event this is
|
33
|
-
# @param data [Hash] representing the event-specific data to be passed to the observers
|
34
|
-
def notify event_type, **data
|
35
|
-
Event.new(type: event_type, data: data).tap do |event|
|
36
|
-
self << event
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
# Add an observer to this pipe
|
41
|
-
# @param callable [Proc] (optional)
|
42
|
-
# @param &block [Block] (optional)
|
43
|
-
# @return an object representing this observer (dependent upon the implementation of the pipe itself)
|
44
|
-
# Either a `callable` or a `block` must be supplied. If the latter, it is converted to a [Proc]
|
45
|
-
def add_observer callable = nil, &block
|
46
|
-
callable ||= block.to_proc
|
47
|
-
Types::Observer[callable].tap do |observer|
|
48
|
-
@observers << observer
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
# Remove an observer from this pipe
|
53
|
-
# @param observer
|
54
|
-
# This removes the given observer from this pipe. The observer should have previously been returned by #add_observer and is implementation-specific
|
55
|
-
def remove_observer observer
|
56
|
-
@observers.delete observer
|
57
|
-
end
|
58
|
-
|
59
|
-
# Test whether the given observer is observing this pipe
|
60
|
-
# @param observer
|
61
|
-
# @return [boolean]
|
62
|
-
def is_observer? observer
|
63
|
-
@observers.include? observer
|
64
|
-
end
|
65
|
-
|
66
|
-
# Close this pipe and perform any cleanup.
|
67
|
-
# Subclasses should override this to perform their own shutdown routines and call `super` to ensure everything is tidied up
|
68
|
-
def shutdown
|
69
|
-
# clean up and release any observers, just in case
|
70
|
-
@observers = []
|
71
|
-
end
|
72
|
-
|
73
|
-
# Start this pipe
|
74
|
-
# Subclasses may override this method to add any implementation specific details.
|
75
|
-
# By default any supplied parameters are called to the subclass' `initialize` method
|
76
|
-
def self.start(**params)
|
77
|
-
new(**params)
|
78
|
-
end
|
79
|
-
|
80
|
-
protected
|
81
|
-
|
82
|
-
# Get the next event from the queue
|
83
|
-
# @return [Plumbing::Event]
|
84
|
-
# Subclasses should implement this method
|
85
|
-
def get_next_event
|
86
|
-
raise PipeIsBlocked
|
87
|
-
end
|
88
|
-
|
89
|
-
# Start the event loop
|
90
|
-
# This loop keeps running until `shutdown` is called
|
91
|
-
# Some subclasses may need to replace this method to deal with their own specific implementations
|
92
|
-
# @param initial_event [Plumbing::Event] optional; the first event in the queue
|
93
|
-
def start_run_loop initial_event = nil
|
94
|
-
loop do
|
95
|
-
event = initial_event || get_next_event
|
96
|
-
break if event == :shutdown
|
97
|
-
dispatch event
|
98
|
-
initial_event = nil
|
99
|
-
end
|
100
|
-
end
|
101
|
-
|
102
|
-
# Dispatch an event to all observers
|
103
|
-
# @param event [Plumbing::Event]
|
104
|
-
# Enumerates all observers and `calls` them with this event
|
105
|
-
# Discards any errors raised by the observer so that all observers will be successfully notified
|
106
|
-
def dispatch event
|
107
|
-
@observers.collect do |observer|
|
108
|
-
observer.call event
|
109
|
-
rescue => ex
|
110
|
-
puts ex
|
111
|
-
ex
|
112
|
-
end
|
113
|
-
end
|
114
|
-
end
|
115
|
-
end
|
@@ -1,72 +0,0 @@
|
|
1
|
-
require "dry/types"
|
2
|
-
require_relative "../blocked_pipe"
|
3
|
-
|
4
|
-
module Plumbing
|
5
|
-
module Concurrent
|
6
|
-
class Pipe < Plumbing::BlockedPipe
|
7
|
-
module Types
|
8
|
-
include Dry::Types()
|
9
|
-
# Observers must be Ractors
|
10
|
-
Observer = Instance(Ractor)
|
11
|
-
end
|
12
|
-
|
13
|
-
def initialize
|
14
|
-
super
|
15
|
-
@queue = Ractor.new(self) do |instance|
|
16
|
-
while (message = Ractor.receive) != :shutdown
|
17
|
-
case message.first
|
18
|
-
when :add_observer then instance.send :add_observing_ractor, message.last
|
19
|
-
when :is_observer? then Ractor.yield(instance.send(:is_observing_ractor?, message.last))
|
20
|
-
when :remove_observer then instance.send :remove_observing_ractor, message.last
|
21
|
-
else instance.send :dispatch, message.last
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
def add_observer ractor = nil, &block
|
28
|
-
Plumbing::Concurrent::Pipe::Types::Observer[ractor].tap do |observer|
|
29
|
-
@queue << [:add_observer, observer]
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
def remove_observer ractor = nil, &block
|
34
|
-
@queue << [:remove_observer, ractor]
|
35
|
-
end
|
36
|
-
|
37
|
-
def is_observer? ractor
|
38
|
-
@queue << [:is_observer?, ractor]
|
39
|
-
@queue.take
|
40
|
-
end
|
41
|
-
|
42
|
-
def << event
|
43
|
-
@queue << [:dispatch, Plumbing::BlockedPipe::Types::Event[event]]
|
44
|
-
end
|
45
|
-
|
46
|
-
def shutdown
|
47
|
-
@queue << :shutdown
|
48
|
-
super
|
49
|
-
end
|
50
|
-
|
51
|
-
private
|
52
|
-
|
53
|
-
def dispatch event
|
54
|
-
@observers.each do |observer|
|
55
|
-
observer << event
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
def add_observing_ractor observer
|
60
|
-
@observers << observer
|
61
|
-
end
|
62
|
-
|
63
|
-
def is_observing_ractor? observer
|
64
|
-
@observers.include? observer
|
65
|
-
end
|
66
|
-
|
67
|
-
def remove_observing_ractor observer
|
68
|
-
@observers.delete observer
|
69
|
-
end
|
70
|
-
end
|
71
|
-
end
|
72
|
-
end
|