standard-procedure-plumbing 0.2.1 → 0.2.2

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: 00f89cef3ca3db2daf437446af1788755906434de2ec2d4e6e81130b26190f84
4
- data.tar.gz: 44d27c6f52de7bbc24c488f741035d0a9fe4341036c423e51b5031896ca9f50a
3
+ metadata.gz: 6074870313ece34eb4b9565602db4de70bb5b7e14e8a9051d85bb46b6fc64bf5
4
+ data.tar.gz: a81292b0ad9e87dfcce61d531e891c1b4ed25ccda3331ceb731c082c6e9c7c16
5
5
  SHA512:
6
- metadata.gz: 6785c35e5596df5718a8ce458ced0713abbe3d18516a90b6353c806da8f867d3bf4d48d70b8b31b15b7c23a491fa2c855c091250aa96bdf97595f813b53e8e18
7
- data.tar.gz: 22169d08af47f789dd5a950635ad46598c6a77f402a2221ed719f369df00b065185b402f14ba2dd2dd82b54cac8774d67d1adc2c07524307a644be11707d7956
6
+ metadata.gz: 45827e921f7bc272e0688a6477d405a81e8c9ea64405078782fbf5aa45658066bf7f9c5503692f4f6eadfdbb08d21221eb4e0b0731654e4808e0f7ec1111f9bb
7
+ data.tar.gz: 1ba319545accf7051393845b1883d5eb69aeacba182c86f26ca6e95e7761711e922904d2ec5427872e8dcd6ccdf5fba25737fe5c20a887cc06ba23ab2184c81a
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## [0.2.2] - 2024-08-25
2
+
3
+ - Added Plumbing::RubberDuck
4
+
1
5
  ## [0.2.1] - 2024-08-25
2
6
 
3
7
  - Split the Pipe implementation between the Pipe and EventDispatcher
data/README.md CHANGED
@@ -58,6 +58,7 @@ BuildSequence.new.call ["extra element"]
58
58
  # => Plumbing::PostconditionError("must_have_three_elements")
59
59
  ```
60
60
 
61
+
61
62
  ## Plumbing::Pipe - a composable observer
62
63
 
63
64
  [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)).
@@ -180,6 +181,37 @@ end
180
181
  ```
181
182
 
182
183
 
184
+ ## Plumbing::RubberDuck - duck types and type-casts
185
+
186
+ Define an [interface or protocol](https://en.wikipedia.org/wiki/Interface_(object-oriented_programming) specifying which messages you expect to be able to send. Then cast an object into that type, which first tests that the object can respond to those messages and limits you to sending those messages and no others.
187
+
188
+
189
+ ### Usage
190
+
191
+ Define your interface (Person in this example), then cast your objects (instances of PersonData and CarData).
192
+
193
+ ```ruby
194
+ require "plumbing"
195
+
196
+ Person = Plumbing::RubberDuck.define :first_name, :last_name, :email
197
+
198
+ PersonData = Struct.new(:first_name, :last_name, :email, :favourite_food)
199
+ CarData = Struct.new(:make, :model, :colour)
200
+
201
+ @porsche_911 = CarData.new "Porsche", "911", "black"
202
+ @person = @porsche_911.as Person
203
+ # => Raises a TypeError
204
+
205
+ @alice = PersonData.new "Alice", "Aardvark", "alice@example.com", "Ice cream"
206
+ @person = @alice.as Person
207
+ @person.first_name
208
+ # => "Alice"
209
+ @person.email
210
+ # => "alice@example.com"
211
+ @person.favourite_food
212
+ # => NoMethodError - even though :favourite_food is a field in PersonData, it is not included in the definition of Person so cannot be accessed through the RubberDuck type
213
+ ```
214
+
183
215
  ## Installation
184
216
 
185
217
  Install the gem and add to the application's Gemfile by executing:
@@ -194,7 +226,6 @@ Then:
194
226
  require 'plumbing'
195
227
  ```
196
228
 
197
-
198
229
  ## Development
199
230
 
200
231
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -0,0 +1 @@
1
+ 2309f738a6739650f259c456af69796e59c1de214579a8a274d56b7ccc8176e9e59cb0e63d744bd5a6ed40e93f4c57a368c3de2bc525163e90da9467a58c6387
@@ -10,10 +10,4 @@ module Plumbing
10
10
 
11
11
  # Error raised because an invalid [Event] object was pushed into the pipe
12
12
  class InvalidEvent < Error; end
13
-
14
- # Error raised because an invalid observer was registered
15
- class InvalidObserver < Error; end
16
-
17
- # Error raised because a Pipe was connected to a non-Pipe
18
- class InvalidSource < Plumbing::Error; end
19
13
  end
@@ -47,7 +47,7 @@ module Plumbing
47
47
  end
48
48
 
49
49
  def dispatch_event event, task
50
- @observers.collect do |observer|
50
+ @observers.each do |observer|
51
51
  task.async do
52
52
  observer.call event
53
53
  rescue => ex
@@ -1,13 +1,12 @@
1
1
  module Plumbing
2
2
  class EventDispatcher
3
3
  def initialize observers: []
4
- @observers = observers
4
+ @observers = observers.as(Collection)
5
5
  end
6
6
 
7
7
  def add_observer observer = nil, &block
8
8
  observer ||= block.to_proc
9
- raise Plumbing::InvalidObserver.new "observer_does_not_respond_to_call" unless observer.respond_to? :call
10
- @observers << observer
9
+ @observers << observer.as(Callable).target
11
10
  observer
12
11
  end
13
12
 
@@ -20,7 +19,7 @@ module Plumbing
20
19
  end
21
20
 
22
21
  def dispatch event
23
- @observers.collect do |observer|
22
+ @observers.each do |observer|
24
23
  observer.call event
25
24
  rescue => ex
26
25
  puts ex
@@ -6,9 +6,8 @@ module Plumbing
6
6
  # @param &accepts [Block] a block that returns a boolean value - true to accept the event, false to reject it
7
7
  def initialize source:, dispatcher: nil, &accepts
8
8
  super(dispatcher: dispatcher)
9
- raise InvalidSource.new "#{source} must be a Plumbing::Pipe descendant" unless source.is_a? Plumbing::Pipe
10
- @accepts = accepts
11
- source.add_observer do |event|
9
+ @accepts = accepts.as(Callable)
10
+ source.as(Observable).add_observer do |event|
12
11
  filter_and_republish event
13
12
  end
14
13
  end
@@ -11,8 +11,7 @@ module Plumbing
11
11
  private
12
12
 
13
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|
14
+ source.as(Observable).add_observer do |event|
16
15
  dispatch event
17
16
  end
18
17
  source
data/lib/plumbing/pipe.rb CHANGED
@@ -5,7 +5,7 @@ module Plumbing
5
5
 
6
6
  # Subclasses should call `super()` to ensure the pipe is initialised corrected
7
7
  def initialize dispatcher: nil
8
- @dispatcher = dispatcher || EventDispatcher.new
8
+ @dispatcher = dispatcher.nil? ? EventDispatcher.new : dispatcher.as(DispatchesEvents)
9
9
  end
10
10
 
11
11
  # Push an event into the pipe
@@ -25,19 +25,19 @@ module Plumbing
25
25
 
26
26
  def validate_contract_for input
27
27
  return true if @validation_contract.nil?
28
- result = const_get(@validation_contract).new.call(input)
28
+ result = const_get(@validation_contract).new.as(Callable).call(input)
29
29
  raise PreConditionError, result.errors.to_h.to_yaml unless result.success?
30
30
  input
31
31
  end
32
32
 
33
33
  def validate_preconditions_for input
34
- failed_preconditions = pre_conditions.select { |name, validator| !validator.call(input) }
34
+ failed_preconditions = pre_conditions.select { |name, validator| !validator.as(Callable).call(input) }
35
35
  raise PreConditionError, failed_preconditions.keys.join(", ") if failed_preconditions.any?
36
36
  input
37
37
  end
38
38
 
39
39
  def validate_postconditions_for output
40
- failed_postconditions = post_conditions.select { |name, validator| !validator.call(output) }
40
+ failed_postconditions = post_conditions.select { |name, validator| !validator.as(Callable).call(output) }
41
41
  raise PostConditionError, failed_postconditions.keys.join(", ") if failed_postconditions.any?
42
42
  output
43
43
  end
@@ -18,7 +18,7 @@ module Plumbing
18
18
  validate_preconditions_for input
19
19
  result = input
20
20
  operations.each do |operation|
21
- result = operation.call(result, instance)
21
+ result = operation.as(Callable).call(result, instance)
22
22
  end
23
23
  validate_postconditions_for result
24
24
  result
@@ -37,7 +37,7 @@ module Plumbing
37
37
 
38
38
  def perform_external method, class_or_class_name
39
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) }
40
+ implementation = ->(input, instance) { external_class.new.as(Callable).call(input) }
41
41
  operations << implementation
42
42
  end
43
43
  end
@@ -0,0 +1,9 @@
1
+ module Plumbing
2
+ class RubberDuck
3
+ ::Object.class_eval do
4
+ def as duck_type
5
+ duck_type.proxy_for self
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,16 @@
1
+ module Plumbing
2
+ class RubberDuck
3
+ class Proxy
4
+ attr_reader :target
5
+
6
+ def initialize target, duck_type
7
+ @target = target
8
+ @duck_type = duck_type
9
+ end
10
+
11
+ def as duck_type
12
+ (duck_type == @duck_type) ? self : duck_type.proxy_for(target)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,50 @@
1
+ module Plumbing
2
+ # A type-checker for duck-types
3
+ class RubberDuck
4
+ require_relative "rubber_duck/object"
5
+ require_relative "rubber_duck/proxy"
6
+
7
+ def initialize *methods
8
+ @methods = methods.map(&:to_sym)
9
+ @proxy_classes = {}
10
+ end
11
+
12
+ def verify object
13
+ missing_methods = @methods.reject { |method| object.respond_to? method }
14
+ raise TypeError, "Expected object to respond to #{missing_methods.join(", ")}" unless missing_methods.empty?
15
+ object
16
+ end
17
+
18
+ def proxy_for object
19
+ is_a_proxy?(object) || build_proxy_for(object)
20
+ end
21
+
22
+ def self.define *methods
23
+ new(*methods)
24
+ end
25
+
26
+ private
27
+
28
+ def is_a_proxy? object
29
+ @proxy_classes.value?(object.class) ? object : nil
30
+ end
31
+
32
+ def build_proxy_for object
33
+ proxy_class_for(object).new(verify(object), self)
34
+ end
35
+
36
+ def proxy_class_for object
37
+ @proxy_classes[object.class] ||= define_proxy_class_for(object.class)
38
+ end
39
+
40
+ def define_proxy_class_for klass
41
+ Class.new(Plumbing::RubberDuck::Proxy).tap do |proxy_class|
42
+ @methods.each do |method|
43
+ proxy_class.define_method method do |*args, &block|
44
+ @target.send method, *args, &block
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,6 @@
1
+ module Plumbing
2
+ Callable = RubberDuck.define :call
3
+ Observable = RubberDuck.define :add_observer, :remove_observer, :is_observer?
4
+ DispatchesEvents = RubberDuck.define :add_observer, :remove_observer, :is_observer?, :shutdown, :dispatch
5
+ Collection = RubberDuck.define :each, :<<, :delete, :include?
6
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Plumbing
4
- VERSION = "0.2.1"
4
+ VERSION = "0.2.2"
5
5
  end
data/lib/plumbing.rb CHANGED
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
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/junction"
10
- require_relative "plumbing/pipeline"
11
-
12
3
  module Plumbing
4
+ require_relative "plumbing/rubber_duck"
5
+ require_relative "plumbing/types"
6
+ require_relative "plumbing/error"
7
+ require_relative "plumbing/event"
8
+ require_relative "plumbing/pipe"
9
+ require_relative "plumbing/filter"
10
+ require_relative "plumbing/junction"
11
+ require_relative "plumbing/pipeline"
12
+ require_relative "plumbing/version"
13
13
  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.2.1
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rahoul Baruah
@@ -31,6 +31,7 @@ files:
31
31
  - checksums/standard-procedure-plumbing-0.1.2.gem.sha512
32
32
  - checksums/standard-procedure-plumbing-0.2.0.gem.sha512
33
33
  - checksums/standard-procedure-plumbing-0.2.1.gem.sha512
34
+ - checksums/standard-procedure-plumbing-0.2.2.gem.sha512
34
35
  - lib/plumbing.rb
35
36
  - lib/plumbing/error.rb
36
37
  - lib/plumbing/event.rb
@@ -42,6 +43,10 @@ files:
42
43
  - lib/plumbing/pipeline.rb
43
44
  - lib/plumbing/pipeline/contracts.rb
44
45
  - lib/plumbing/pipeline/operations.rb
46
+ - lib/plumbing/rubber_duck.rb
47
+ - lib/plumbing/rubber_duck/object.rb
48
+ - lib/plumbing/rubber_duck/proxy.rb
49
+ - lib/plumbing/types.rb
45
50
  - lib/plumbing/version.rb
46
51
  - sig/plumbing.rbs
47
52
  homepage: https://github.com/standard-procedure/plumbing