standard-procedure-plumbing 0.3.1 → 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +119 -47
  3. data/lib/plumbing/rubber_duck/module.rb +13 -0
  4. data/lib/plumbing/rubber_duck/object.rb +3 -2
  5. data/lib/plumbing/rubber_duck.rb +14 -4
  6. data/lib/plumbing/valve/rails.rb +15 -0
  7. data/lib/plumbing/valve/threaded.rb +67 -0
  8. data/lib/plumbing/version.rb +1 -1
  9. data/spec/become_equal_to_matcher.rb +25 -0
  10. data/spec/examples/pipe_spec.rb +109 -0
  11. data/spec/examples/pipeline_spec.rb +89 -0
  12. data/spec/examples/rubber_duck_spec.rb +109 -0
  13. data/spec/examples/valve_spec.rb +81 -0
  14. data/spec/plumbing/a_pipe.rb +106 -0
  15. data/spec/plumbing/custom_filter_spec.rb +29 -0
  16. data/spec/plumbing/filter_spec.rb +32 -0
  17. data/spec/plumbing/junction_spec.rb +67 -0
  18. data/spec/plumbing/pipe_spec.rb +31 -0
  19. data/spec/plumbing/pipeline_spec.rb +208 -0
  20. data/spec/plumbing/rubber_duck_spec.rb +74 -0
  21. data/spec/plumbing/valve_spec.rb +171 -0
  22. data/spec/plumbing_spec.rb +7 -0
  23. data/spec/spec_helper.rb +16 -0
  24. metadata +21 -19
  25. data/.rspec +0 -3
  26. data/.rubocop.yml +0 -24
  27. data/.solargraph.yml +0 -32
  28. data/.standard.yml +0 -9
  29. data/.vscode/tasks.json +0 -11
  30. data/CHANGELOG.md +0 -40
  31. data/CODE_OF_CONDUCT.md +0 -5
  32. data/LICENSE +0 -504
  33. data/checksums/standard-procedure-plumbing-0.1.1.gem.sha512 +0 -1
  34. data/checksums/standard-procedure-plumbing-0.1.2.gem.sha512 +0 -1
  35. data/checksums/standard-procedure-plumbing-0.2.0.gem.sha512 +0 -1
  36. data/checksums/standard-procedure-plumbing-0.2.1.gem.sha512 +0 -1
  37. data/checksums/standard-procedure-plumbing-0.2.2.gem.sha512 +0 -1
  38. data/checksums/standard-procedure-plumbing-0.3.0.gem.sha512 +0 -1
  39. data/checksums/standard-procedure-plumbing-0.3.1.gem.sha512 +0 -1
  40. data/sig/plumbing.rbs +0 -4
@@ -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,109 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe "Rubber Duck examples" do
4
+ it "casts objects into duck types" do
5
+ # standard:disable Lint/ConstantDefinitionInBlock
6
+ module DuckExample
7
+ Person = Plumbing::RubberDuck.define :first_name, :last_name, :email
8
+ LikesFood = Plumbing::RubberDuck.define :favourite_food
9
+
10
+ PersonData = Struct.new(:first_name, :last_name, :email, :favourite_food)
11
+ CarData = Struct.new(:make, :model, :colour)
12
+ end
13
+
14
+ # standard:enable Lint/ConstantDefinitionInBlock
15
+
16
+ @porsche_911 = DuckExample::CarData.new "Porsche", "911", "black"
17
+ expect { @porsche_911.as DuckExample::Person }.to raise_error(TypeError)
18
+
19
+ @alice = DuckExample::PersonData.new "Alice", "Aardvark", "alice@example.com", "Ice cream"
20
+
21
+ @person = @alice.as DuckExample::Person
22
+ expect(@person.first_name).to eq "Alice"
23
+ expect(@person.email).to eq "alice@example.com"
24
+ expect { @person.favourite_food }.to raise_error(NoMethodError)
25
+
26
+ @hungry = @person.as DuckExample::LikesFood
27
+ expect(@hungry.favourite_food).to eq "Ice cream"
28
+ end
29
+
30
+ it "casts objects into modules" do
31
+ # standard:disable Lint/ConstantDefinitionInBlock
32
+ module ModuleExample
33
+ module Person
34
+ def first_name = @first_name
35
+
36
+ def last_name = @last_name
37
+
38
+ def email = @email
39
+ end
40
+
41
+ module LikesFood
42
+ def favourite_food = @favourite_food
43
+ end
44
+
45
+ PersonData = Struct.new(:first_name, :last_name, :email, :favourite_food)
46
+ CarData = Struct.new(:make, :model, :colour)
47
+ end
48
+ # standard:enable Lint/ConstantDefinitionInBlock
49
+ @porsche_911 = ModuleExample::CarData.new "Porsche", "911", "black"
50
+ expect { @porsche_911.as ModuleExample::Person }.to raise_error(TypeError)
51
+
52
+ @alice = ModuleExample::PersonData.new "Alice", "Aardvark", "alice@example.com", "Ice cream"
53
+
54
+ @person = @alice.as ModuleExample::Person
55
+ expect(@person.first_name).to eq "Alice"
56
+ expect(@person.email).to eq "alice@example.com"
57
+ expect { @person.favourite_food }.to raise_error(NoMethodError)
58
+
59
+ @hungry = @person.as ModuleExample::LikesFood
60
+ expect(@hungry.favourite_food).to eq "Ice cream"
61
+ end
62
+
63
+ it "casts objects into clases" do
64
+ # standard:disable Lint/ConstantDefinitionInBlock
65
+ module ClassExample
66
+ class Person
67
+ def initialize first_name, last_name, email
68
+ @first_name = first_name
69
+ @last_name = last_name
70
+ @email = email
71
+ end
72
+
73
+ attr_reader :first_name
74
+ attr_reader :last_name
75
+ attr_reader :email
76
+ end
77
+
78
+ class PersonWhoLikesFood < Person
79
+ def initialize first_name, last_name, email, favourite_food
80
+ super(first_name, last_name, email)
81
+ @favourite_food = favourite_food
82
+ end
83
+
84
+ attr_reader :favourite_food
85
+ end
86
+
87
+ class CarData
88
+ def initialize make, model, colour
89
+ @make = make
90
+ @model = model
91
+ @colour = colour
92
+ end
93
+ end
94
+ end
95
+ # standard:enable Lint/ConstantDefinitionInBlock
96
+ @porsche_911 = ClassExample::CarData.new "Porsche", "911", "black"
97
+ expect { @porsche_911.as ClassExample::Person }.to raise_error(TypeError)
98
+
99
+ @alice = ClassExample::PersonWhoLikesFood.new "Alice", "Aardvark", "alice@example.com", "Ice cream"
100
+
101
+ @person = @alice.as ClassExample::Person
102
+ expect(@person.first_name).to eq "Alice"
103
+ expect(@person.email).to eq "alice@example.com"
104
+ expect { @person.favourite_food }.to raise_error(NoMethodError)
105
+
106
+ @hungry = @person.as ClassExample::PersonWhoLikesFood
107
+ expect(@hungry.favourite_food).to eq "Ice cream"
108
+ end
109
+ end
@@ -0,0 +1,81 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.shared_examples "an example valve" do |runs_in_background|
4
+ it "queries an object" do
5
+ @person = Employee.start "Alice"
6
+
7
+ expect(@person.name).to eq "Alice"
8
+ expect(@person.job_title).to eq "Sales assistant"
9
+
10
+ @time = Time.now
11
+ # `greet_slowly` is a query so will block until a response is received
12
+ expect(@person.greet_slowly).to eq "H E L L O"
13
+ expect(Time.now - @time).to be > 0.1
14
+
15
+ @time = Time.now
16
+ # we're ignoring the result so this will not block (except :inline mode which does not run in the background)
17
+ expect(@person.greet_slowly(ignore_result: true)).to be_nil
18
+ expect(Time.now - @time).to be < 0.1 if runs_in_background
19
+ expect(Time.now - @time).to be > 0.1 if !runs_in_background
20
+ end
21
+
22
+ it "commands an object" do
23
+ @person = Employee.start "Alice"
24
+ @person.promote
25
+ @job_title = @person.job_title
26
+ expect(@job_title).to eq "Sales manager"
27
+ end
28
+ end
29
+
30
+ RSpec.describe "Valve example: " do
31
+ # standard:disable Lint/ConstantDefinitionInBlock
32
+ class Employee
33
+ include Plumbing::Valve
34
+ query :name, :job_title, :greet_slowly
35
+ command :promote
36
+
37
+ def initialize(name)
38
+ @name = name
39
+ @job_title = "Sales assistant"
40
+ end
41
+
42
+ attr_reader :name, :job_title
43
+
44
+ def promote
45
+ sleep 0.5
46
+ @job_title = "Sales manager"
47
+ end
48
+
49
+ def greet_slowly
50
+ sleep 0.2
51
+ "H E L L O"
52
+ end
53
+ end
54
+ # standard:enable Lint/ConstantDefinitionInBlock
55
+
56
+ context "inline mode" do
57
+ around :example do |example|
58
+ Plumbing.configure mode: :inline, &example
59
+ end
60
+
61
+ it_behaves_like "an example valve", false
62
+ end
63
+
64
+ context "async mode" do
65
+ around :example do |example|
66
+ Plumbing.configure mode: :async do
67
+ Kernel::Async(&example)
68
+ end
69
+ end
70
+
71
+ it_behaves_like "an example valve", true
72
+ end
73
+
74
+ context "threaded mode" do
75
+ around :example do |example|
76
+ Plumbing.configure mode: :threaded, &example
77
+ end
78
+
79
+ it_behaves_like "an example valve", true
80
+ end
81
+ 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,31 @@
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
+
24
+ context "threaded" do
25
+ around :example do |example|
26
+ Plumbing.configure mode: :threaded, &example
27
+ end
28
+
29
+ it_behaves_like "a pipe"
30
+ end
31
+ end