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 +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +32 -1
- data/checksums/standard-procedure-plumbing-0.2.2.gem.sha512 +1 -0
- data/lib/plumbing/error.rb +0 -6
- data/lib/plumbing/event_dispatcher/fiber.rb +1 -1
- data/lib/plumbing/event_dispatcher.rb +3 -4
- data/lib/plumbing/filter.rb +2 -3
- data/lib/plumbing/junction.rb +1 -2
- data/lib/plumbing/pipe.rb +1 -1
- data/lib/plumbing/pipeline/contracts.rb +3 -3
- data/lib/plumbing/pipeline/operations.rb +2 -2
- data/lib/plumbing/rubber_duck/object.rb +9 -0
- data/lib/plumbing/rubber_duck/proxy.rb +16 -0
- data/lib/plumbing/rubber_duck.rb +50 -0
- data/lib/plumbing/types.rb +6 -0
- data/lib/plumbing/version.rb +1 -1
- data/lib/plumbing.rb +9 -9
- metadata +6 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6074870313ece34eb4b9565602db4de70bb5b7e14e8a9051d85bb46b6fc64bf5
|
4
|
+
data.tar.gz: a81292b0ad9e87dfcce61d531e891c1b4ed25ccda3331ceb731c082c6e9c7c16
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 45827e921f7bc272e0688a6477d405a81e8c9ea64405078782fbf5aa45658066bf7f9c5503692f4f6eadfdbb08d21221eb4e0b0731654e4808e0f7ec1111f9bb
|
7
|
+
data.tar.gz: 1ba319545accf7051393845b1883d5eb69aeacba182c86f26ca6e95e7761711e922904d2ec5427872e8dcd6ccdf5fba25737fe5c20a887cc06ba23ab2184c81a
|
data/CHANGELOG.md
CHANGED
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
|
data/lib/plumbing/error.rb
CHANGED
@@ -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
|
@@ -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
|
-
|
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.
|
22
|
+
@observers.each do |observer|
|
24
23
|
observer.call event
|
25
24
|
rescue => ex
|
26
25
|
puts ex
|
data/lib/plumbing/filter.rb
CHANGED
@@ -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
|
-
|
10
|
-
|
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
|
data/lib/plumbing/junction.rb
CHANGED
@@ -11,8 +11,7 @@ module Plumbing
|
|
11
11
|
private
|
12
12
|
|
13
13
|
def add source
|
14
|
-
|
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
|
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,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
|
data/lib/plumbing/version.rb
CHANGED
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.
|
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
|