standard-procedure-plumbing 0.2.2 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1 @@
1
+ 966c4bcf848d26272cf0b3bb3d74c9724aa9899a8bcbf5e25a6ce20608de1f529dc766056aa0cb99fbe1c7d6e16afe72acbcc0619fc14f57f01eb7921be7b653
@@ -0,0 +1,51 @@
1
+ # Pipes, pipelines, valves and rubber ducks
2
+ module Plumbing
3
+ Config = Data.define :mode, :valve_proxy_classes, :timeout do
4
+ def valve_proxy_class_for target_class
5
+ valve_proxy_classes[target_class]
6
+ end
7
+
8
+ def register_valve_proxy_class_for target_class, proxy_class
9
+ valve_proxy_classes[target_class] = proxy_class
10
+ end
11
+ end
12
+ private_constant :Config
13
+
14
+ # Access the current configuration
15
+ # @return [Config]
16
+ def self.config
17
+ configs.last
18
+ end
19
+
20
+ # Configure the plumbing
21
+ # @param params [Hash] the configuration options
22
+ # @option mode [Symbol] the mode to use (:inline is the default, :async uses fibers)
23
+ # @option timeout [Integer] the timeout (in seconds) to use (30s is the default)
24
+ # @yield optional block - after the block has completed its execution, the configuration is restored to its previous state (useful for test suites)
25
+ def self.configure(**params, &block)
26
+ new_config = Config.new(**config.to_h.merge(params).merge(valve_proxy_classes: {}))
27
+ if block.nil?
28
+ set_configuration_to new_config
29
+ else
30
+ set_configuration_and_yield new_config, &block
31
+ end
32
+ end
33
+
34
+ def self.set_configuration_to config
35
+ configs << config
36
+ end
37
+ private_class_method :set_configuration_to
38
+
39
+ def self.set_configuration_and_yield(new_config, &block)
40
+ set_configuration_to new_config
41
+ yield
42
+ ensure
43
+ configs.pop
44
+ end
45
+ private_class_method :set_configuration_and_yield
46
+
47
+ def self.configs
48
+ @configs ||= [Config.new(mode: :inline, timeout: 30, valve_proxy_classes: {})]
49
+ end
50
+ private_class_method :configs
51
+ end
@@ -0,0 +1,15 @@
1
+ module Plumbing
2
+ # A pipe that can be subclassed to filter events from a source pipe
3
+ class CustomFilter < Pipe
4
+ # Chain this pipe to the source pipe
5
+ # @param source [Plumbing::Observable] the source from which to receive and filter events
6
+ def initialize source:
7
+ super()
8
+ source.as(Observable).add_observer { |event| received event }
9
+ end
10
+
11
+ protected
12
+
13
+ def received(event) = raise NoMethodError.new("Subclass should define #received")
14
+ end
15
+ end
@@ -1,5 +1,4 @@
1
1
  module Plumbing
2
2
  # An immutable data structure representing an Event
3
- Event = Data.define :type, :data do
4
- end
3
+ Event = Data.define :type, :data
5
4
  end
@@ -1,20 +1,20 @@
1
+ require_relative "custom_filter"
1
2
  module Plumbing
2
3
  # A pipe that filters events from a source pipe
3
- class Filter < Pipe
4
+ class Filter < CustomFilter
4
5
  # Chain this pipe to the source pipe
5
- # @param source [Plumbing::Pipe]
6
+ # @param source [Plumbing::Observable] the source from which to receive and filter events
6
7
  # @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)
8
+ # @yield [Plumbing::Event] event the event that is currently being processed
9
+ # @yieldreturn [Boolean] true to accept the event, false to reject it
10
+ def initialize source:, &accepts
11
+ super(source: source)
9
12
  @accepts = accepts.as(Callable)
10
- source.as(Observable).add_observer do |event|
11
- filter_and_republish event
12
- end
13
13
  end
14
14
 
15
- private
15
+ protected
16
16
 
17
- def filter_and_republish event
17
+ def received(event)
18
18
  return nil unless @accepts.call event
19
19
  dispatch event
20
20
  end
@@ -2,9 +2,9 @@ module Plumbing
2
2
  # A pipe that filters events from a source pipe
3
3
  class Junction < Pipe
4
4
  # Chain multiple sources to this pipe
5
- # @param [Array<Plumbing::Pipe>]
6
- def initialize *sources, dispatcher: nil
7
- super(dispatcher: dispatcher)
5
+ # @param sources [Array<Plumbing::Observable>] the sources which will be joined and relayed
6
+ def initialize *sources
7
+ super()
8
8
  @sources = sources.collect { |source| add(source) }
9
9
  end
10
10
 
data/lib/plumbing/pipe.rb CHANGED
@@ -1,12 +1,10 @@
1
1
  module Plumbing
2
2
  # A basic pipe
3
3
  class Pipe
4
- require_relative "event_dispatcher"
4
+ include Plumbing::Valve
5
5
 
6
- # Subclasses should call `super()` to ensure the pipe is initialised corrected
7
- def initialize dispatcher: nil
8
- @dispatcher = dispatcher.nil? ? EventDispatcher.new : dispatcher.as(DispatchesEvents)
9
- end
6
+ command :notify, :<<, :remove_observer, :shutdown
7
+ query :add_observer, :is_observer?
10
8
 
11
9
  # Push an event into the pipe
12
10
  # @param event [Plumbing::Event] the event to push into the pipe
@@ -25,40 +23,33 @@ module Plumbing
25
23
  end
26
24
 
27
25
  # Add an observer to this pipe
28
- # @param callable [Proc] (optional)
29
- # @param &block [Block] (optional)
30
- # @return an object representing this observer (dependent upon the implementation of the pipe itself)
31
26
  # Either a `callable` or a `block` must be supplied. If the latter, it is converted to a [Proc]
32
- def add_observer(observer = nil, &)
33
- @dispatcher.add_observer(observer, &)
27
+ # @param callable [#call] (optional)
28
+ # @param &block [Block] (optional)
29
+ # @return [#call]
30
+ def add_observer(observer = nil, &block)
31
+ observer ||= block.to_proc
32
+ observers << observer.as(Callable).target
33
+ observer
34
34
  end
35
35
 
36
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
37
+ # @param observer [#call] remove the observer from this pipe (where the observer was previously added by #add_observer)
39
38
  def remove_observer observer
40
- @dispatcher.remove_observer observer
39
+ observers.delete observer
41
40
  end
42
41
 
43
42
  # Test whether the given observer is observing this pipe
44
- # @param observer
45
- # @return [boolean]
43
+ # @param [#call] observer
44
+ # @return [Boolean]
46
45
  def is_observer? observer
47
- @dispatcher.is_observer? observer
46
+ observers.include? observer
48
47
  end
49
48
 
50
49
  # Close this pipe and perform any cleanup.
51
50
  # Subclasses should override this to perform their own shutdown routines and call `super` to ensure everything is tidied up
52
51
  def shutdown
53
- # clean up and release any observers, just in case
54
- @dispatcher.shutdown
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(*, **, &)
61
- new(*, **, &)
52
+ observers.clear
62
53
  end
63
54
 
64
55
  protected
@@ -68,7 +59,16 @@ module Plumbing
68
59
  # Enumerates all observers and `calls` them with this event
69
60
  # Discards any errors raised by the observer so that all observers will be successfully notified
70
61
  def dispatch event
71
- @dispatcher.dispatch event
62
+ observers.each do |observer|
63
+ observer.call event
64
+ rescue => ex
65
+ puts ex
66
+ ex
67
+ end
68
+ end
69
+
70
+ def observers
71
+ @observers ||= []
72
72
  end
73
73
  end
74
74
  end
@@ -1,14 +1,24 @@
1
1
  module Plumbing
2
2
  class Pipeline
3
+ # Validate input and output data with pre and post conditions or [Dry::Validation::Contract]s
3
4
  module Contracts
5
+ # @param name [Symbol] the name of the precondition
6
+ # @param &validator [Block] a block that returns a boolean value - true to accept the input, false to reject it
7
+ # @yield [Object] input the input data to be validated
8
+ # @yieldreturn [Boolean] true to accept the input, false to reject it
4
9
  def pre_condition name, &validator
5
10
  pre_conditions[name.to_sym] = validator
6
11
  end
7
12
 
13
+ # @param [String] contract_class the class name of the [Dry::Validation::Contract] that will be used to validate the input data
8
14
  def validate_with contract_class
9
15
  @validation_contract = contract_class
10
16
  end
11
17
 
18
+ # @param name [Symbol] the name of the postcondition
19
+ # @param &validator [Block] a block that returns a boolean value - true to accept the input, false to reject it
20
+ # @yield [Object] output the output data to be validated
21
+ # @yieldreturn [Boolean] true to accept the output, false to reject it
12
22
  def post_condition name, &validator
13
23
  post_conditions[name.to_sym] = validator
14
24
  end
@@ -1,10 +1,26 @@
1
1
  module Plumbing
2
2
  class Pipeline
3
+ # Defining the operations that will be performed on the input data
3
4
  module Operations
5
+ # Add an operation to the pipeline
6
+ # Operations are processed in order, unless interrupted by an exception
7
+ # The output from the previous operation is fed in as the input to the this operation
8
+ # and the output from this operation is fed in as the input to the next operation
9
+ #
10
+ # @param method [Symbol] the method to be called on the input data
11
+ # @param using [String, Class] the optional class name or class that will be used to perform the operation
12
+ # @param &implementation [Block] the optional block that will be used to perform the operation (instead of calling a method)
13
+ # @yield [Object] input the input data to be processed
14
+ # @yieldreturn [Object] the output data
4
15
  def perform method, using: nil, &implementation
5
16
  using.nil? ? perform_internal(method, &implementation) : perform_external(method, using)
6
17
  end
7
18
 
19
+ # Add an operation which does not alter the input data to the pipeline
20
+ # The output from the previous operation is fed in as the input to the this operation
21
+ # but the output from this operation is discarded and the previous input is fed in to the next operation
22
+ #
23
+ # @param method [Symbol] the method to be called on the input data
8
24
  def execute method
9
25
  implementation ||= ->(input, instance) do
10
26
  instance.send(method, input)
@@ -13,6 +29,7 @@ module Plumbing
13
29
  operations << implementation
14
30
  end
15
31
 
32
+ # Internal use only
16
33
  def _call input, instance
17
34
  validate_contract_for input
18
35
  validate_preconditions_for input
@@ -7,6 +7,9 @@ module Plumbing
7
7
  extend Plumbing::Pipeline::Contracts
8
8
  extend Plumbing::Pipeline::Operations
9
9
 
10
+ # Start the pipeline operation with the given input
11
+ # @param input [Object] the input data to be processed
12
+ # @return [Object] the output data
10
13
  def call input
11
14
  self.class._call input, self
12
15
  end
@@ -1,6 +1,8 @@
1
1
  module Plumbing
2
2
  class RubberDuck
3
3
  ::Object.class_eval do
4
+ # Cast the object to a duck-type
5
+ # @return [Plumbing::RubberDuck::Proxy] the duck-type proxy
4
6
  def as duck_type
5
7
  duck_type.proxy_for self
6
8
  end
@@ -1,5 +1,6 @@
1
1
  module Plumbing
2
2
  class RubberDuck
3
+ # Proxy object that forwards the duck-typed methods to the target object
3
4
  class Proxy
4
5
  attr_reader :target
5
6
 
@@ -8,6 +9,8 @@ module Plumbing
8
9
  @duck_type = duck_type
9
10
  end
10
11
 
12
+ # Convert the proxy to the given duck-type, ensuring that existing proxies are not duplicated
13
+ # @return [Plumbing::RubberDuck::Proxy] the proxy for the given duck-type
11
14
  def as duck_type
12
15
  (duck_type == @duck_type) ? self : duck_type.proxy_for(target)
13
16
  end
@@ -9,16 +9,25 @@ module Plumbing
9
9
  @proxy_classes = {}
10
10
  end
11
11
 
12
+ # Verify that the given object responds to the required methods
13
+ # @param object [Object] the object to verify
14
+ # @return [Object] the object if it passes the verification
15
+ # @raise [TypeError] if the object does not respond to the required methods
12
16
  def verify object
13
17
  missing_methods = @methods.reject { |method| object.respond_to? method }
14
18
  raise TypeError, "Expected object to respond to #{missing_methods.join(", ")}" unless missing_methods.empty?
15
19
  object
16
20
  end
17
21
 
22
+ # Test if the given object is a proxy
23
+ # @param object [Object] the object to test
24
+ # @return [Boolean] true if the object is a proxy, false otherwise
18
25
  def proxy_for object
19
26
  is_a_proxy?(object) || build_proxy_for(object)
20
27
  end
21
28
 
29
+ # Define a new rubber duck type
30
+ # @param *methods [Array<Symbol>] the methods that the duck-type should respond to
22
31
  def self.define *methods
23
32
  new(*methods)
24
33
  end
@@ -0,0 +1,43 @@
1
+ require "async"
2
+ require "async/semaphore"
3
+ require "timeout"
4
+
5
+ module Plumbing
6
+ module Valve
7
+ class Async
8
+ attr_reader :target
9
+
10
+ def initialize target
11
+ @target = target
12
+ @queue = []
13
+ @semaphore = ::Async::Semaphore.new(1)
14
+ end
15
+
16
+ # Ask the target to answer the given message
17
+ def ask(message, *args, **params, &block)
18
+ task = @semaphore.async do
19
+ @target.send message, *args, **params, &block
20
+ end
21
+ Timeout.timeout(timeout) do
22
+ task.wait
23
+ end
24
+ end
25
+
26
+ # Tell the target to execute the given message
27
+ def tell(message, *args, **params, &block)
28
+ @semaphore.async do |task|
29
+ @target.send message, *args, **params, &block
30
+ rescue
31
+ nil
32
+ end
33
+ nil
34
+ end
35
+
36
+ private
37
+
38
+ def timeout
39
+ Plumbing.config.timeout
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,20 @@
1
+ module Plumbing
2
+ module Valve
3
+ class Inline
4
+ def initialize target
5
+ @target = target
6
+ end
7
+
8
+ # Ask the target to answer the given message
9
+ def ask(message, ...)
10
+ @target.send(message, ...)
11
+ end
12
+
13
+ # Tell the target to execute the given message
14
+ def tell(message, ...)
15
+ @target.send(message, ...)
16
+ nil
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,5 @@
1
+ module Plumbing
2
+ module Valve
3
+ Message = Struct.new :message, :args, :params, :block, :result, :status
4
+ end
5
+ end
@@ -0,0 +1,71 @@
1
+ require_relative "valve/inline"
2
+
3
+ module Plumbing
4
+ module Valve
5
+ def self.included base
6
+ base.extend ClassMethods
7
+ end
8
+
9
+ module ClassMethods
10
+ # Create a new valve instance and build a proxy for it using the current mode
11
+ # @return [Plumbing::Valve::Base] the proxy for the valve instance
12
+ def start(*, **, &)
13
+ build_proxy_for(new(*, **, &))
14
+ end
15
+
16
+ # Define the queries that this valve can answer
17
+ # @param names [Array<Symbol>] the names of the queries
18
+ def query(*names) = queries.concat(names.map(&:to_sym))
19
+
20
+ # List the queries that this valve can answer
21
+ def queries = @queries ||= []
22
+
23
+ # Define the commands that this valve can execute
24
+ # @param names [Array<Symbol>] the names of the commands
25
+ def command(*names) = commands.concat(names.map(&:to_sym))
26
+
27
+ # List the commands that this valve can execute
28
+ def commands = @commands ||= []
29
+
30
+ def inherited subclass
31
+ subclass.commands.concat commands
32
+ subclass.queries.concat queries
33
+ end
34
+
35
+ private
36
+
37
+ def build_proxy_for(target)
38
+ proxy_class_for(target.class).new(target)
39
+ end
40
+
41
+ def proxy_class_for target_class
42
+ Plumbing.config.valve_proxy_class_for(target_class) || register_valve_proxy_class_for(target_class)
43
+ end
44
+
45
+ def proxy_base_class = const_get "Plumbing::Valve::#{Plumbing.config.mode.to_s.capitalize}"
46
+
47
+ def register_valve_proxy_class_for target_class
48
+ Plumbing.config.register_valve_proxy_class_for(target_class, build_proxy_class)
49
+ end
50
+
51
+ def build_proxy_class
52
+ Class.new(proxy_base_class).tap do |proxy_class|
53
+ queries.each do |query|
54
+ proxy_class.define_method query do |*args, ignore_result: false, **params, &block|
55
+ ignore_result ? tell(query, *args, **params, &block) : ask(query, *args, **params, &block)
56
+ end
57
+ end
58
+
59
+ commands.each do |command|
60
+ proxy_class.define_method command do |*args, **params, &block|
61
+ tell(command, *args, **params, &block)
62
+ nil
63
+ rescue
64
+ nil
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Plumbing
4
- VERSION = "0.2.2"
4
+ VERSION = "0.3.1"
5
5
  end
data/lib/plumbing.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Plumbing
4
+ require_relative "plumbing/config"
5
+ require_relative "plumbing/valve"
4
6
  require_relative "plumbing/rubber_duck"
5
7
  require_relative "plumbing/types"
6
8
  require_relative "plumbing/error"
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.2
4
+ version: 0.3.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-25 00:00:00.000000000 Z
11
+ date: 2024-09-03 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: A composable event pipeline and sequential pipelines of operations
14
14
  email:
@@ -32,11 +32,13 @@ files:
32
32
  - checksums/standard-procedure-plumbing-0.2.0.gem.sha512
33
33
  - checksums/standard-procedure-plumbing-0.2.1.gem.sha512
34
34
  - checksums/standard-procedure-plumbing-0.2.2.gem.sha512
35
+ - checksums/standard-procedure-plumbing-0.3.0.gem.sha512
36
+ - checksums/standard-procedure-plumbing-0.3.1.gem.sha512
35
37
  - lib/plumbing.rb
38
+ - lib/plumbing/config.rb
39
+ - lib/plumbing/custom_filter.rb
36
40
  - lib/plumbing/error.rb
37
41
  - lib/plumbing/event.rb
38
- - lib/plumbing/event_dispatcher.rb
39
- - lib/plumbing/event_dispatcher/fiber.rb
40
42
  - lib/plumbing/filter.rb
41
43
  - lib/plumbing/junction.rb
42
44
  - lib/plumbing/pipe.rb
@@ -47,6 +49,10 @@ files:
47
49
  - lib/plumbing/rubber_duck/object.rb
48
50
  - lib/plumbing/rubber_duck/proxy.rb
49
51
  - lib/plumbing/types.rb
52
+ - lib/plumbing/valve.rb
53
+ - lib/plumbing/valve/async.rb
54
+ - lib/plumbing/valve/inline.rb
55
+ - lib/plumbing/valve/message.rb
50
56
  - lib/plumbing/version.rb
51
57
  - sig/plumbing.rbs
52
58
  homepage: https://github.com/standard-procedure/plumbing
@@ -1,61 +0,0 @@
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.each 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
@@ -1,34 +0,0 @@
1
- module Plumbing
2
- class EventDispatcher
3
- def initialize observers: []
4
- @observers = observers.as(Collection)
5
- end
6
-
7
- def add_observer observer = nil, &block
8
- observer ||= block.to_proc
9
- @observers << observer.as(Callable).target
10
- observer
11
- end
12
-
13
- def remove_observer observer
14
- @observers.delete observer
15
- end
16
-
17
- def is_observer? observer
18
- @observers.include? observer
19
- end
20
-
21
- def dispatch event
22
- @observers.each do |observer|
23
- observer.call event
24
- rescue => ex
25
- puts ex
26
- ex
27
- end
28
- end
29
-
30
- def shutdown
31
- @observers = []
32
- end
33
- end
34
- end