standard-procedure-plumbing 0.3.1 → 0.3.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: 9a111ce599942b5e33b6928cede476b75e56fd6574625bf50ca82d6d75f3e718
4
- data.tar.gz: b1b4c48062aec9d77ec3b917c25723fae8ff2cc4075d9f91c699da3f165b7c84
3
+ metadata.gz: 54681502a7df050136406e706c4fd6b9d9bffea81e44c151e969379e6803c532
4
+ data.tar.gz: ed03cbc9476d43822792fd6a4788b0c2e6c629545056063342f15c0861dc7dd0
5
5
  SHA512:
6
- metadata.gz: 6d2de706d57ef380e67fcd9d79b3d202ca0741f7ed24552ad62bc63944f1c0a82cdd6123560a6d79a85099218db5c9c966ee602a0ef281ea7d5b0305e1f5a707
7
- data.tar.gz: 47de86307b817c0d0399bc06bcac5f78eea3629b93725b785f1dc6a442a300a03ceadde6627f2d198ea296524f584f29a90fa3ec52e5e635507c024241fec5da
6
+ metadata.gz: 1387fdd83a547157541f444ccf2dcb9465c261616f424dc530ef4c9c13dda52214c21c4d50c99eb825e4827268f4892d62a29d212af9be21bc5bd65dd83422a4
7
+ data.tar.gz: c0dcbde3271e4d34cd41d16bda9c488ee40bd838ac0f006fd46bd23eb2c0c1cc4ea18d86236de1d8e87f6b5b97fc5dc728bedbd5a3e354fa09886094866404c9
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Plumbing
4
- VERSION = "0.3.1"
4
+ VERSION = "0.3.2"
5
5
  end
@@ -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
@@ -0,0 +1,23 @@
1
+ require "spec_helper"
2
+ require_relative "a_pipe"
3
+ require "async"
4
+
5
+ RSpec.describe Plumbing::Pipe do
6
+ context "inline" do
7
+ around :example do |example|
8
+ Plumbing.configure mode: :inline, &example
9
+ end
10
+
11
+ it_behaves_like "a pipe"
12
+ end
13
+
14
+ context "async" do
15
+ around :example do |example|
16
+ Sync do
17
+ Plumbing.configure mode: :async, &example
18
+ end
19
+ end
20
+
21
+ it_behaves_like "a pipe"
22
+ end
23
+ end