standard-procedure-plumbing 0.3.1 → 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 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