standard-procedure-plumbing 0.1.2 → 0.2.0

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