standard-procedure-plumbing 0.1.2 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a0bfd8678cd958f92a30d6c1879e664ac790637b400a2fd73302a7ed60a3b450
4
- data.tar.gz: f06824216f1d2da14fa645b0f246106180e6e8e69f8fa7d5526e8a343ac77923
3
+ metadata.gz: 71e621c375dc0a17f928884eee2718fc7ed89a2f113374e90a5e6532914d71ea
4
+ data.tar.gz: 6632624c924bdee49e9dc4b273da36792916c434d12b867d874bcd1ddb880b8b
5
5
  SHA512:
6
- metadata.gz: ac2e9872aba1ecb6c4f2831ee6da3689afdcf1162c5bc0b65eaa2298ae1a00f4d698fb7909c34e10daa6b14cd10bcd8ab54cd92839fab7a1107cd93de831c2c5
7
- data.tar.gz: 64f585e329a112504b692b788d6b6c9562bc188eb9c621a33001f2aed69d92c6260909c8a3df174436bfeda345791b7ca370afb09fdad2d281d466efcb0784fc
6
+ metadata.gz: aa02b3cc5688ebeed1ec98f22142077d9c26e952bc34772d9395d6d120f2a5103efc67a5cc0e3f7508c937f6392a294f3d6088d8adca9d7415cf81cb2e7f28e6
7
+ data.tar.gz: d2a164087e839fc3cd48d9b0c22cee9765541a388e4ffc48f5f614faa4690b69d7aa17a6bfc6d77d03ce2ed91e2af49cb8a02fa98e094117194414d7dbd49470
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ## [0.2.0] - 2024-08-14
2
+
3
+ - Added optional Dry::Validation support
4
+ - Use Async for fiber-based pipes
5
+
1
6
  ## [0.1.2] - 2024-08-14
2
7
 
3
8
  - Removed dependencies
data/README.md CHANGED
@@ -99,7 +99,15 @@ end
99
99
 
100
100
  Define a sequence of operations that proceed in order, passing their output from one operation as the input to another.
101
101
 
102
- You can define pre-conditions (which validate the inputs supplied) or post-conditions (which validate the output).
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`.
103
111
 
104
112
  ### Usage:
105
113
 
@@ -107,11 +115,12 @@ You can define pre-conditions (which validate the inputs supplied) or post-condi
107
115
  require "plumbing"
108
116
  class BuildSequence < Plumbing::Chain
109
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
110
119
  input.is_a? Array
111
120
  end
112
121
 
113
122
  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
123
+ # yes, this is a stupid post-condition but 🤷🏾‍♂️
115
124
  output.length == 3
116
125
  end
117
126
 
@@ -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
@@ -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
- def call params
5
- self.class._call params, self
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
- def validate_postconditions_for output
53
- failed_postconditions = post_conditions.select { |name, validator| !validator.call(output) }
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
@@ -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
@@ -1,29 +1,26 @@
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
3
+ class Filter < Pipe
6
4
  class InvalidFilter < Error; end
7
5
 
8
6
  # Chain this pipe to the source pipe
9
- # @param source [Plumbing::BlockedPipe]
7
+ # @param source [Plumbing::Pipe]
10
8
  # @param accepts [Array[String]] event types that this filter will allow through (or pass [] to allow all)
11
9
  # @param rejects [Array[String]] event types that this filter will not allow through
12
10
  def initialize source:, accepts: [], rejects: []
13
11
  super()
14
- raise InvalidFilter.new "source must be a Plumbing::BlockedPipe descendant" unless source.is_a? Plumbing::BlockedPipe
12
+ raise InvalidFilter.new "source must be a Plumbing::Pipe descendant" unless source.is_a? Plumbing::Pipe
15
13
  raise InvalidFilter.new "accepts and rejects must be arrays" unless accepts.is_a?(Array) && rejects.is_a?(Array)
16
14
  @accepted_event_types = accepts
17
15
  @rejected_event_types = rejects
18
16
  source.add_observer do |event|
19
- filter_and_republish(event)
17
+ filter_and_republish event
20
18
  end
21
19
  end
22
20
 
23
21
  private
24
22
 
25
23
  def filter_and_republish event
26
- raise InvalidEvent.new "event is not a Plumbing::Event" unless event.is_a? Plumbing::Event
27
24
  return nil if @accepted_event_types.any? && !@accepted_event_types.include?(event.type)
28
25
  return nil if @rejected_event_types.include? event.type
29
26
  dispatch event
data/lib/plumbing/pipe.rb CHANGED
@@ -1,29 +1,79 @@
1
- require_relative "blocked_pipe"
2
-
3
1
  module Plumbing
4
- # An implementation of a pipe that uses Fibers
5
- class Pipe < BlockedPipe
2
+ # A basic pipe
3
+ class Pipe
4
+ # Subclasses should call `super()` to ensure the pipe is initialised corrected
6
5
  def initialize
7
- super
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
- raise Plumbing::InvalidEvent.new "event is not a Plumbing::Event" unless event.is_a? Plumbing::Event
15
- @fiber.resume event
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
16
34
  end
17
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
18
52
  def shutdown
19
- super
20
- @fiber.resume :shutdown
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)
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
+ @observers.collect do |observer|
72
+ observer.call event
73
+ rescue => ex
74
+ puts ex
75
+ ex
76
+ end
27
77
  end
28
78
  end
29
79
  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.0"
5
5
  end
data/lib/plumbing.rb CHANGED
@@ -1,10 +1,12 @@
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/chain"
5
10
 
6
- require_relative "plumbing/error"
7
- require_relative "plumbing/event"
8
- require_relative "plumbing/pipe"
9
- require_relative "plumbing/chain"
11
+ module Plumbing
10
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.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rahoul Baruah
@@ -29,19 +29,21 @@ files:
29
29
  - checksums/standard-procedure-plumbing-0.1.1.gem.sha512
30
30
  - checksums/standard-procedure-plumbing-0.1.2.gem.sha512
31
31
  - lib/plumbing.rb
32
- - lib/plumbing/blocked_pipe.rb
33
32
  - lib/plumbing/chain.rb
33
+ - lib/plumbing/chain/contracts.rb
34
+ - lib/plumbing/chain/operations.rb
34
35
  - lib/plumbing/error.rb
35
36
  - lib/plumbing/event.rb
37
+ - lib/plumbing/fiber/pipe.rb
36
38
  - lib/plumbing/filter.rb
37
39
  - lib/plumbing/pipe.rb
38
40
  - lib/plumbing/version.rb
39
41
  - sig/plumbing.rbs
40
- homepage: https://theartandscienceofruby.com
42
+ homepage: https://github.com/standard-procedure/plumbing
41
43
  licenses: []
42
44
  metadata:
43
45
  allowed_push_host: https://rubygems.org
44
- homepage_uri: https://theartandscienceofruby.com
46
+ homepage_uri: https://github.com/standard-procedure/plumbing
45
47
  source_code_uri: https://github.com/standard-procedure/plumbing
46
48
  changelog_uri: https://github.com/standard-procedure/plumbing/blob/main/CHANGELOG.md
47
49
  post_install_message:
@@ -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