standard-procedure-plumbing 0.3.0 → 0.3.2
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 +4 -4
- data/README.md +24 -2
- data/lib/plumbing/valve/async.rb +1 -0
- data/lib/plumbing/valve/inline.rb +1 -0
- data/lib/plumbing/valve.rb +2 -2
- data/lib/plumbing/version.rb +1 -1
- data/spec/become_equal_to_matcher.rb +25 -0
- data/spec/examples/pipe_spec.rb +109 -0
- data/spec/examples/pipeline_spec.rb +89 -0
- data/spec/examples/rubber_duck_spec.rb +26 -0
- data/spec/examples/valve_spec.rb +88 -0
- data/spec/plumbing/a_pipe.rb +106 -0
- data/spec/plumbing/custom_filter_spec.rb +29 -0
- data/spec/plumbing/filter_spec.rb +32 -0
- data/spec/plumbing/junction_spec.rb +67 -0
- data/spec/plumbing/pipe_spec.rb +23 -0
- data/spec/plumbing/pipeline_spec.rb +208 -0
- data/spec/plumbing/rubber_duck_spec.rb +76 -0
- data/spec/plumbing/valve_spec.rb +167 -0
- data/spec/plumbing_spec.rb +7 -0
- data/spec/spec_helper.rb +16 -0
- metadata +17 -17
- data/.rspec +0 -3
- data/.rubocop.yml +0 -24
- data/.solargraph.yml +0 -32
- data/.standard.yml +0 -9
- data/.vscode/tasks.json +0 -11
- data/CHANGELOG.md +0 -36
- data/CODE_OF_CONDUCT.md +0 -5
- data/LICENSE +0 -504
- data/checksums/standard-procedure-plumbing-0.1.1.gem.sha512 +0 -1
- data/checksums/standard-procedure-plumbing-0.1.2.gem.sha512 +0 -1
- data/checksums/standard-procedure-plumbing-0.2.0.gem.sha512 +0 -1
- data/checksums/standard-procedure-plumbing-0.2.1.gem.sha512 +0 -1
- data/checksums/standard-procedure-plumbing-0.2.2.gem.sha512 +0 -1
- data/checksums/standard-procedure-plumbing-0.3.0.gem.sha512 +0 -1
- data/sig/plumbing.rbs +0 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 54681502a7df050136406e706c4fd6b9d9bffea81e44c151e969379e6803c532
|
4
|
+
data.tar.gz: ed03cbc9476d43822792fd6a4788b0c2e6c629545056063342f15c0861dc7dd0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1387fdd83a547157541f444ccf2dcb9465c261616f424dc530ef4c9c13dda52214c21c4d50c99eb825e4827268f4892d62a29d212af9be21bc5bd65dd83422a4
|
7
|
+
data.tar.gz: c0dcbde3271e4d34cd41d16bda9c488ee40bd838ac0f006fd46bd23eb2c0c1cc4ea18d86236de1d8e87f6b5b97fc5dc728bedbd5a3e354fa09886094866404c9
|
data/README.md
CHANGED
@@ -156,7 +156,12 @@ An [actor](https://en.wikipedia.org/wiki/Actor_model) defines the messages an ob
|
|
156
156
|
|
157
157
|
[Plumbing::Valve](/lib/plumbing/valve.rb) ensures that all messages received are channelled into a concurrency-safe queue. This allows you to take an existing class and ensures that messages received via its public API are made concurrency-safe.
|
158
158
|
|
159
|
-
Include the Plumbing::Valve module into your class, define the messages the objects can respond to and set the `Plumbing` configuration to set the desired concurrency model. Messages themselves are split into two categories: commands and queries.
|
159
|
+
Include the Plumbing::Valve module into your class, define the messages the objects can respond to and set the `Plumbing` configuration to set the desired concurrency model. Messages themselves are split into two categories: commands and queries.
|
160
|
+
|
161
|
+
- Commands have no return value so when the message is sent, the caller does not block, the task is called asynchronously and the caller continues immediately
|
162
|
+
- Queries return a value so the caller blocks until the actor has returned a value
|
163
|
+
- However, if you call a query and pass `ignore_result: true` then the query will not block, although you will not be able to access the return value - this is for commands that do something and then return a result based on that work (which you may or may not be interested in - see Plumbing::Pipe#add_observer)
|
164
|
+
- None of the above applies if the `Plumbing mode` is set to `:inline` (which is the default) - in this case, the actor behaves like normal ruby code
|
160
165
|
|
161
166
|
Instead of constructing your object with `.new`, use `.start`. This builds a proxy object that wraps the target instance and dispatches messages through a safe mechanism. Only messages that have been defined as part of the valve are available in this proxy - so you don't have to worry about callers bypassing the valve's internal context.
|
162
167
|
|
@@ -175,7 +180,7 @@ Also be aware that if you use valves in one place, you need to use them everywhe
|
|
175
180
|
attr_reader :name, :job_title
|
176
181
|
|
177
182
|
include Plumbing::Valve
|
178
|
-
query :name, :job_title
|
183
|
+
query :name, :job_title, :greet_slowly
|
179
184
|
command :promote
|
180
185
|
|
181
186
|
def initialize(name)
|
@@ -187,6 +192,11 @@ Also be aware that if you use valves in one place, you need to use them everywhe
|
|
187
192
|
sleep 0.5
|
188
193
|
@job_title = "Sales manager"
|
189
194
|
end
|
195
|
+
|
196
|
+
def greet_slowly
|
197
|
+
sleep 0.2
|
198
|
+
"H E L L O"
|
199
|
+
end
|
190
200
|
end
|
191
201
|
```
|
192
202
|
|
@@ -206,6 +216,12 @@ Also be aware that if you use valves in one place, you need to use them everywhe
|
|
206
216
|
# this will block for 0.5 seconds
|
207
217
|
puts @person.job_title
|
208
218
|
# => "Sales manager"
|
219
|
+
|
220
|
+
@person.greet_slowly
|
221
|
+
# this will block for 0.2 seconds before returning "H E L L O"
|
222
|
+
|
223
|
+
@person.greet_slowly(ignore_result: true)
|
224
|
+
# this will block for 0.2 seconds (as the mode is :inline) before returning nil
|
209
225
|
```
|
210
226
|
|
211
227
|
[Using fibers](/spec/examples/valve_spec.rb) with concurrency but no parallelism
|
@@ -226,6 +242,12 @@ Also be aware that if you use valves in one place, you need to use them everywhe
|
|
226
242
|
# this will return immediately without blocking
|
227
243
|
puts @person.job_title
|
228
244
|
# => "Sales manager" (this will block for 0.5s because #job_title query will not start until the #promote command has completed)
|
245
|
+
|
246
|
+
@person.greet_slowly
|
247
|
+
# this will block for 0.2 seconds before returning "H E L L O"
|
248
|
+
|
249
|
+
@person.greet_slowly(ignore_result: true)
|
250
|
+
# this will not block and returns nil
|
229
251
|
```
|
230
252
|
|
231
253
|
## Plumbing::Pipe - a composable observer
|
data/lib/plumbing/valve/async.rb
CHANGED
data/lib/plumbing/valve.rb
CHANGED
@@ -51,8 +51,8 @@ module Plumbing
|
|
51
51
|
def build_proxy_class
|
52
52
|
Class.new(proxy_base_class).tap do |proxy_class|
|
53
53
|
queries.each do |query|
|
54
|
-
proxy_class.define_method query do |*args, **params, &block|
|
55
|
-
ask(query, *args, **params, &block)
|
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
56
|
end
|
57
57
|
end
|
58
58
|
|
data/lib/plumbing/version.rb
CHANGED
@@ -0,0 +1,25 @@
|
|
1
|
+
require "rspec/expectations"
|
2
|
+
|
3
|
+
# Custom matcher that repeatedly evaluates the block until it matches the expected value or 5 seconds have elapsed
|
4
|
+
#
|
5
|
+
# This allows asynchronous operations to be tested in a synchronous manner with a timeout
|
6
|
+
#
|
7
|
+
# Example:
|
8
|
+
# expect("Hello").to become_equal_to { subject.greeting }
|
9
|
+
#
|
10
|
+
RSpec::Matchers.define :become_equal_to do
|
11
|
+
match do |expected|
|
12
|
+
counter = 0
|
13
|
+
matched = false
|
14
|
+
while (counter < 50) && (matched == false)
|
15
|
+
matched = true if (@result = block_arg.call) == expected
|
16
|
+
sleep 0.1
|
17
|
+
counter += 1
|
18
|
+
end
|
19
|
+
matched
|
20
|
+
end
|
21
|
+
|
22
|
+
failure_message do |expected|
|
23
|
+
"expected block to return #{expected} but was #{@result} after timeout expired"
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "async"
|
3
|
+
|
4
|
+
RSpec.describe "Pipe examples" do
|
5
|
+
it "observes events" do
|
6
|
+
@source = Plumbing::Pipe.start
|
7
|
+
|
8
|
+
@result = []
|
9
|
+
@observer = @source.add_observer do |event|
|
10
|
+
@result << event.type
|
11
|
+
end
|
12
|
+
|
13
|
+
@source.notify "something_happened", message: "But what was it?"
|
14
|
+
expect(@result).to eq ["something_happened"]
|
15
|
+
end
|
16
|
+
|
17
|
+
it "filters events" do
|
18
|
+
@source = Plumbing::Pipe.start
|
19
|
+
|
20
|
+
@filter = Plumbing::Filter.start source: @source do |event|
|
21
|
+
%w[important urgent].include? event.type
|
22
|
+
end
|
23
|
+
|
24
|
+
@result = []
|
25
|
+
@observer = @filter.add_observer do |event|
|
26
|
+
@result << event.type
|
27
|
+
end
|
28
|
+
|
29
|
+
@source.notify "important", message: "ALERT! ALERT!"
|
30
|
+
expect(@result).to eq ["important"]
|
31
|
+
|
32
|
+
@source.notify "unimportant", message: "Nothing to see here"
|
33
|
+
expect(@result).to eq ["important"]
|
34
|
+
end
|
35
|
+
|
36
|
+
it "allows for custom filters" do
|
37
|
+
# standard:disable Lint/ConstantDefinitionInBlock
|
38
|
+
class EveryThirdEvent < Plumbing::CustomFilter
|
39
|
+
def initialize source:
|
40
|
+
super
|
41
|
+
@events = []
|
42
|
+
end
|
43
|
+
|
44
|
+
def received event
|
45
|
+
@events << event
|
46
|
+
if @events.count >= 3
|
47
|
+
@events.clear
|
48
|
+
self << event
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
# standard:enable Lint/ConstantDefinitionInBlock
|
53
|
+
|
54
|
+
@source = Plumbing::Pipe.start
|
55
|
+
@filter = EveryThirdEvent.new(source: @source)
|
56
|
+
|
57
|
+
@result = []
|
58
|
+
@observer = @filter.add_observer do |event|
|
59
|
+
@result << event.type
|
60
|
+
end
|
61
|
+
|
62
|
+
1.upto 10 do |i|
|
63
|
+
@source.notify i.to_s
|
64
|
+
end
|
65
|
+
|
66
|
+
expect(@result).to eq ["3", "6", "9"]
|
67
|
+
end
|
68
|
+
|
69
|
+
it "joins multiple source pipes" do
|
70
|
+
@first_source = Plumbing::Pipe.start
|
71
|
+
@second_source = Plumbing::Pipe.start
|
72
|
+
|
73
|
+
@junction = Plumbing::Junction.start @first_source, @second_source
|
74
|
+
|
75
|
+
@result = []
|
76
|
+
@observer = @junction.add_observer do |event|
|
77
|
+
@result << event.type
|
78
|
+
end
|
79
|
+
|
80
|
+
@first_source.notify "one"
|
81
|
+
expect(@result).to eq ["one"]
|
82
|
+
@second_source.notify "two"
|
83
|
+
expect(@result).to eq ["one", "two"]
|
84
|
+
end
|
85
|
+
|
86
|
+
it "dispatches events asynchronously using fibers" do
|
87
|
+
Plumbing.configure mode: :async do
|
88
|
+
Sync do
|
89
|
+
@first_source = Plumbing::Pipe.start
|
90
|
+
@second_source = Plumbing::Pipe.start
|
91
|
+
@junction = Plumbing::Junction.start @first_source, @second_source
|
92
|
+
@filter = Plumbing::Filter.start source: @junction do |event|
|
93
|
+
%w[one-one two-two].include? event.type
|
94
|
+
end
|
95
|
+
@result = []
|
96
|
+
@filter.add_observer do |event|
|
97
|
+
@result << event.type
|
98
|
+
end
|
99
|
+
|
100
|
+
@first_source.notify "one-one"
|
101
|
+
@first_source.notify "one-two"
|
102
|
+
@second_source.notify "two-one"
|
103
|
+
@second_source.notify "two-two"
|
104
|
+
|
105
|
+
expect(["one-one", "two-two"]).to become_equal_to { @result }
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "dry/validation"
|
3
|
+
|
4
|
+
RSpec.describe "Pipeline examples" do
|
5
|
+
it "builds a simple pipeline of operations adding to an array with pre-conditions and post-conditions" do
|
6
|
+
# standard:disable Lint/ConstantDefinitionInBlock
|
7
|
+
class BuildArray < Plumbing::Pipeline
|
8
|
+
perform :add_first
|
9
|
+
perform :add_second
|
10
|
+
perform :add_third
|
11
|
+
|
12
|
+
pre_condition :must_be_an_array do |input|
|
13
|
+
input.is_a? Array
|
14
|
+
end
|
15
|
+
|
16
|
+
post_condition :must_have_three_elements do |output|
|
17
|
+
output.length == 3
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def add_first(input) = input << "first"
|
23
|
+
|
24
|
+
def add_second(input) = input << "second"
|
25
|
+
|
26
|
+
def add_third(input) = input << "third"
|
27
|
+
end
|
28
|
+
# standard:enable Lint/ConstantDefinitionInBlock
|
29
|
+
|
30
|
+
expect(BuildArray.new.call([])).to eq ["first", "second", "third"]
|
31
|
+
expect { BuildArray.new.call(1) }.to raise_error(Plumbing::PreConditionError)
|
32
|
+
expect { BuildArray.new.call(["extra element"]) }.to raise_error(Plumbing::PostConditionError)
|
33
|
+
end
|
34
|
+
|
35
|
+
it "builds a simple pipeline of operations using an external class to implement one of the steps" do
|
36
|
+
# standard:disable Lint/ConstantDefinitionInBlock
|
37
|
+
class ExternalStep < Plumbing::Pipeline
|
38
|
+
perform :add_item_to_array
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def add_item_to_array(input) = input << "external"
|
43
|
+
end
|
44
|
+
|
45
|
+
class BuildSequenceWithExternalStep < Plumbing::Pipeline
|
46
|
+
perform :add_first
|
47
|
+
perform :add_second, using: "ExternalStep"
|
48
|
+
perform :add_third
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def add_first(input) = input << "first"
|
53
|
+
|
54
|
+
def add_third(input) = input << "third"
|
55
|
+
end
|
56
|
+
# standard:enable Lint/ConstantDefinitionInBlock
|
57
|
+
|
58
|
+
expect(BuildSequenceWithExternalStep.new.call([])).to eq ["first", "external", "third"]
|
59
|
+
end
|
60
|
+
|
61
|
+
it "uses a dry-validation contract to test the input parameters" do
|
62
|
+
# standard:disable Lint/ConstantDefinitionInBlock
|
63
|
+
class SayHello < Plumbing::Pipeline
|
64
|
+
validate_with "SayHello::Input"
|
65
|
+
perform :say_hello
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def say_hello input
|
70
|
+
"Hello #{input[:name]} - I will now send a load of annoying marketing messages to #{input[:email]}"
|
71
|
+
end
|
72
|
+
|
73
|
+
class Input < Dry::Validation::Contract
|
74
|
+
params do
|
75
|
+
required(:name).filled(:string)
|
76
|
+
required(:email).filled(:string)
|
77
|
+
end
|
78
|
+
rule :email do
|
79
|
+
key.failure("must be a valid email") unless /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i.match? value
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
# standard:enable Lint/ConstantDefinitionInBlock
|
84
|
+
|
85
|
+
SayHello.new.call(name: "Alice", email: "alice@example.com")
|
86
|
+
|
87
|
+
expect { SayHello.new.call(some: "other data") }.to raise_error(Plumbing::PreConditionError)
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
RSpec.describe "Rubber Duck examples" do
|
4
|
+
it "casts objects as duck types" do
|
5
|
+
# standard:disable Lint/ConstantDefinitionInBlock
|
6
|
+
Person = Plumbing::RubberDuck.define :first_name, :last_name, :email
|
7
|
+
LikesFood = Plumbing::RubberDuck.define :favourite_food
|
8
|
+
|
9
|
+
PersonData = Struct.new(:first_name, :last_name, :email, :favourite_food)
|
10
|
+
CarData = Struct.new(:make, :model, :colour)
|
11
|
+
# standard:enable Lint/ConstantDefinitionInBlock
|
12
|
+
|
13
|
+
@porsche_911 = CarData.new "Porsche", "911", "black"
|
14
|
+
expect { @porsche_911.as Person }.to raise_error(TypeError)
|
15
|
+
|
16
|
+
@alice = PersonData.new "Alice", "Aardvark", "alice@example.com", "Ice cream"
|
17
|
+
|
18
|
+
@person = @alice.as Person
|
19
|
+
expect(@person.first_name).to eq "Alice"
|
20
|
+
expect(@person.email).to eq "alice@example.com"
|
21
|
+
expect { @person.favourite_food }.to raise_error(NoMethodError)
|
22
|
+
|
23
|
+
@hungry = @person.as LikesFood
|
24
|
+
expect(@hungry.favourite_food).to eq "Ice cream"
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
RSpec.describe "Valve examples" do
|
4
|
+
# standard:disable Lint/ConstantDefinitionInBlock
|
5
|
+
class Employee
|
6
|
+
include Plumbing::Valve
|
7
|
+
query :name, :job_title, :greet_slowly
|
8
|
+
command :promote
|
9
|
+
|
10
|
+
def initialize(name)
|
11
|
+
@name = name
|
12
|
+
@job_title = "Sales assistant"
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :name, :job_title
|
16
|
+
|
17
|
+
def promote
|
18
|
+
sleep 0.5
|
19
|
+
@job_title = "Sales manager"
|
20
|
+
end
|
21
|
+
|
22
|
+
def greet_slowly
|
23
|
+
sleep 0.2
|
24
|
+
"H E L L O"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
# standard:enable Lint/ConstantDefinitionInBlock
|
28
|
+
|
29
|
+
context "inline" do
|
30
|
+
it "queries an object" do
|
31
|
+
Plumbing.configure mode: :inline do
|
32
|
+
@person = Employee.start "Alice"
|
33
|
+
|
34
|
+
expect(@person.name).to eq "Alice"
|
35
|
+
expect(@person.job_title).to eq "Sales assistant"
|
36
|
+
|
37
|
+
@time = Time.now
|
38
|
+
expect(@person.greet_slowly).to eq "H E L L O"
|
39
|
+
expect(Time.now - @time).to be > 0.1
|
40
|
+
|
41
|
+
@time = Time.now
|
42
|
+
expect(@person.greet_slowly(ignore_result: true)).to be_nil
|
43
|
+
expect(Time.now - @time).to be > 0.1
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
it "commands an object" do
|
48
|
+
Plumbing.configure mode: :inline do
|
49
|
+
@person = Employee.start "Alice"
|
50
|
+
|
51
|
+
@person.promote
|
52
|
+
|
53
|
+
expect(@person.job_title).to eq "Sales manager"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
context "async" do
|
59
|
+
around :example do |example|
|
60
|
+
Plumbing.configure mode: :async do
|
61
|
+
Kernel::Async(&example)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
it "queries an object" do
|
66
|
+
@person = Employee.start "Alice"
|
67
|
+
|
68
|
+
expect(@person.name).to eq "Alice"
|
69
|
+
expect(@person.job_title).to eq "Sales assistant"
|
70
|
+
|
71
|
+
@time = Time.now
|
72
|
+
expect(@person.greet_slowly).to eq "H E L L O"
|
73
|
+
expect(Time.now - @time).to be > 0.1
|
74
|
+
|
75
|
+
@time = Time.now
|
76
|
+
expect(@person.greet_slowly(ignore_result: true)).to be_nil
|
77
|
+
expect(Time.now - @time).to be < 0.1
|
78
|
+
end
|
79
|
+
|
80
|
+
it "commands an object" do
|
81
|
+
@person = Employee.start "Alice"
|
82
|
+
|
83
|
+
@person.promote
|
84
|
+
|
85
|
+
expect(@person.job_title).to eq "Sales manager"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
RSpec.shared_examples "a pipe" do
|
2
|
+
it "adds a block observer" do
|
3
|
+
@pipe = described_class.start
|
4
|
+
@observer = @pipe.add_observer do |event|
|
5
|
+
puts event.type
|
6
|
+
end
|
7
|
+
expect(@pipe.is_observer?(@observer)).to eq true
|
8
|
+
end
|
9
|
+
|
10
|
+
it "adds a callable observer" do
|
11
|
+
@pipe = described_class.start
|
12
|
+
@proc = ->(event) { puts event.type }
|
13
|
+
|
14
|
+
@pipe.add_observer @proc
|
15
|
+
|
16
|
+
expect(@pipe.is_observer?(@proc)).to eq true
|
17
|
+
end
|
18
|
+
|
19
|
+
it "does not allow an observer without a #call method" do
|
20
|
+
@pipe = described_class.start
|
21
|
+
|
22
|
+
expect { @pipe.add_observer(Object.new) }.to raise_error(TypeError)
|
23
|
+
end
|
24
|
+
|
25
|
+
it "removes an observer" do
|
26
|
+
@pipe = described_class.start
|
27
|
+
@proc = ->(event) { puts event.type }
|
28
|
+
|
29
|
+
@pipe.remove_observer @proc
|
30
|
+
|
31
|
+
expect(@pipe.is_observer?(@proc)).to eq false
|
32
|
+
end
|
33
|
+
|
34
|
+
it "does not send notifications for objects which are not events" do
|
35
|
+
@pipe = described_class.start
|
36
|
+
@results = []
|
37
|
+
@observer = @pipe.add_observer do |event|
|
38
|
+
@results << event
|
39
|
+
end
|
40
|
+
|
41
|
+
@pipe << Object.new
|
42
|
+
|
43
|
+
sleep 0.5
|
44
|
+
expect(@results).to eq []
|
45
|
+
end
|
46
|
+
|
47
|
+
it "notifies block observers" do
|
48
|
+
@pipe = described_class.start
|
49
|
+
@results = []
|
50
|
+
@observer = @pipe.add_observer do |event|
|
51
|
+
@results << event
|
52
|
+
end
|
53
|
+
|
54
|
+
@first_event = Plumbing::Event.new type: "first_event", data: {test: "event"}
|
55
|
+
@second_event = Plumbing::Event.new type: "second_event", data: {test: "event"}
|
56
|
+
|
57
|
+
@pipe << @first_event
|
58
|
+
expect([@first_event]).to become_equal_to { @results }
|
59
|
+
|
60
|
+
@pipe << @second_event
|
61
|
+
expect([@first_event, @second_event]).to become_equal_to { @results }
|
62
|
+
end
|
63
|
+
|
64
|
+
it "notifies callable observers" do
|
65
|
+
@pipe = described_class.start
|
66
|
+
@results = []
|
67
|
+
@observer = ->(event) { @results << event }
|
68
|
+
@pipe.add_observer @observer
|
69
|
+
|
70
|
+
@first_event = Plumbing::Event.new type: "first_event", data: {test: "event"}
|
71
|
+
@second_event = Plumbing::Event.new type: "second_event", data: {test: "event"}
|
72
|
+
|
73
|
+
@pipe << @first_event
|
74
|
+
expect([@first_event]).to become_equal_to { @results }
|
75
|
+
|
76
|
+
@pipe << @second_event
|
77
|
+
expect([@first_event, @second_event]).to become_equal_to { @results }
|
78
|
+
end
|
79
|
+
|
80
|
+
it "ensures all observers are notified even if an observer raises an exception" do
|
81
|
+
@pipe = described_class.start
|
82
|
+
@results = []
|
83
|
+
@failing_observer = @pipe.add_observer do |event|
|
84
|
+
raise "Failed processing #{event.type}"
|
85
|
+
end
|
86
|
+
@working_observer = @pipe.add_observer do |event|
|
87
|
+
@results << event
|
88
|
+
end
|
89
|
+
|
90
|
+
@event = Plumbing::Event.new type: "event", data: {test: "event"}
|
91
|
+
|
92
|
+
@pipe << @event
|
93
|
+
|
94
|
+
expect([@event]).to become_equal_to { @results }
|
95
|
+
end
|
96
|
+
|
97
|
+
it "shuts down the pipe" do
|
98
|
+
@pipe = described_class.start
|
99
|
+
@observer = ->(event) { @results << event }
|
100
|
+
@pipe.add_observer @observer
|
101
|
+
|
102
|
+
@pipe.shutdown
|
103
|
+
|
104
|
+
expect(@pipe.is_observer?(@observer)).to eq false
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
RSpec.describe Plumbing::CustomFilter do
|
4
|
+
it "raises a TypeError if it is connected to a non-Pipe" do
|
5
|
+
@invalid_source = Object.new
|
6
|
+
|
7
|
+
expect { described_class.start source: @invalid_source }.to raise_error(TypeError)
|
8
|
+
end
|
9
|
+
|
10
|
+
it "defines a custom filter" do
|
11
|
+
# standard:disable Lint/ConstantDefinitionInBlock
|
12
|
+
class ReversingFilter < Plumbing::CustomFilter
|
13
|
+
def received(event) = notify event.type.reverse, event.data
|
14
|
+
end
|
15
|
+
# standard:enable Lint/ConstantDefinitionInBlock
|
16
|
+
|
17
|
+
@pipe = Plumbing::Pipe.start
|
18
|
+
@filter = ReversingFilter.new(source: @pipe)
|
19
|
+
@result = []
|
20
|
+
@filter.add_observer do |event|
|
21
|
+
@result << event.type
|
22
|
+
end
|
23
|
+
|
24
|
+
@pipe.notify "hello"
|
25
|
+
@pipe.notify "world"
|
26
|
+
|
27
|
+
expect(@result).to eq ["olleh", "dlrow"]
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
RSpec.describe Plumbing::Filter do
|
4
|
+
it "raises a TypeError if it is connected to a non-Pipe" do
|
5
|
+
@invalid_source = Object.new
|
6
|
+
|
7
|
+
expect { described_class.start source: @invalid_source }.to raise_error(TypeError)
|
8
|
+
end
|
9
|
+
|
10
|
+
it "accepts event types" do
|
11
|
+
@pipe = Plumbing::Pipe.start
|
12
|
+
|
13
|
+
@filter = described_class.start source: @pipe do |event|
|
14
|
+
%w[first_type third_type].include? event.type.to_s
|
15
|
+
end
|
16
|
+
|
17
|
+
@results = []
|
18
|
+
@filter.add_observer do |event|
|
19
|
+
@results << event
|
20
|
+
end
|
21
|
+
|
22
|
+
@pipe << Plumbing::Event.new(type: "first_type", data: nil)
|
23
|
+
expect(@results.count).to eq 1
|
24
|
+
|
25
|
+
@pipe << Plumbing::Event.new(type: "second_type", data: nil)
|
26
|
+
expect(@results.count).to eq 1
|
27
|
+
|
28
|
+
# Use the alternative syntax
|
29
|
+
@pipe.notify "third_type"
|
30
|
+
expect(@results.count).to eq 2
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
RSpec.describe Plumbing::Junction do
|
4
|
+
it "raises a TypeError if it is connected to a non-Pipe" do
|
5
|
+
@sources = [Plumbing::Pipe.start, Object.new, Plumbing::Pipe.start]
|
6
|
+
|
7
|
+
expect { described_class.start(*@sources) }.to raise_error(TypeError)
|
8
|
+
end
|
9
|
+
|
10
|
+
it "publishes events from a single source" do
|
11
|
+
@source = Plumbing::Pipe.start
|
12
|
+
@junction = described_class.start @source
|
13
|
+
|
14
|
+
@results = []
|
15
|
+
@junction.add_observer do |event|
|
16
|
+
@results << event
|
17
|
+
end
|
18
|
+
|
19
|
+
@event = Plumbing::Event.new type: "test_event", data: {test: "event"}
|
20
|
+
@source << @event
|
21
|
+
|
22
|
+
expect([@event]).to become_equal_to { @results }
|
23
|
+
end
|
24
|
+
|
25
|
+
it "publishes events from two sources" do
|
26
|
+
@first_source = Plumbing::Pipe.start
|
27
|
+
@second_source = Plumbing::Pipe.start
|
28
|
+
@junction = described_class.start @first_source, @second_source
|
29
|
+
|
30
|
+
@results = []
|
31
|
+
@junction.add_observer do |event|
|
32
|
+
@results << event
|
33
|
+
end
|
34
|
+
|
35
|
+
@first_event = Plumbing::Event.new type: "test_event", data: {test: "one"}
|
36
|
+
@first_source << @first_event
|
37
|
+
expect([@first_event]).to become_equal_to { @results }
|
38
|
+
|
39
|
+
@second_event = Plumbing::Event.new type: "test_event", data: {test: "two"}
|
40
|
+
@second_source << @second_event
|
41
|
+
expect([@first_event, @second_event]).to become_equal_to { @results }
|
42
|
+
end
|
43
|
+
|
44
|
+
it "publishes events from multiple sources" do
|
45
|
+
@first_source = Plumbing::Pipe.start
|
46
|
+
@second_source = Plumbing::Pipe.start
|
47
|
+
@third_source = Plumbing::Pipe.start
|
48
|
+
@junction = described_class.start @first_source, @second_source, @third_source
|
49
|
+
|
50
|
+
@results = []
|
51
|
+
@junction.add_observer do |event|
|
52
|
+
@results << event
|
53
|
+
end
|
54
|
+
|
55
|
+
@first_event = Plumbing::Event.new type: "test_event", data: {test: "one"}
|
56
|
+
@first_source << @first_event
|
57
|
+
expect([@first_event]).to become_equal_to { @results }
|
58
|
+
|
59
|
+
@second_event = Plumbing::Event.new type: "test_event", data: {test: "two"}
|
60
|
+
@second_source << @second_event
|
61
|
+
expect([@first_event, @second_event]).to become_equal_to { @results }
|
62
|
+
|
63
|
+
@third_event = Plumbing::Event.new type: "test_event", data: {test: "three"}
|
64
|
+
@third_source << @third_event
|
65
|
+
expect([@first_event, @second_event, @third_event]).to become_equal_to { @results }
|
66
|
+
end
|
67
|
+
end
|