standard-procedure-plumbing 0.3.3 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,20 +1,21 @@
1
1
  require "spec_helper"
2
2
 
3
- RSpec.shared_examples "an example valve" do |runs_in_background|
3
+ RSpec.shared_examples "an example actor" do |runs_in_background|
4
4
  it "queries an object" do
5
5
  @person = Employee.start "Alice"
6
6
 
7
- expect(@person.name).to eq "Alice"
8
- expect(@person.job_title).to eq "Sales assistant"
7
+ expect(await { @person.name }).to eq "Alice"
8
+ expect(await { @person.job_title }).to eq "Sales assistant"
9
9
 
10
10
  @time = Time.now
11
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"
12
+ expect(await { @person.greet_slowly }).to eq "H E L L O"
13
13
  expect(Time.now - @time).to be > 0.1
14
14
 
15
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
16
+ # we're not awaiting the result, so this should run in the background (unless we're using inline mode)
17
+ @person.greet_slowly
18
+
18
19
  expect(Time.now - @time).to be < 0.1 if runs_in_background
19
20
  expect(Time.now - @time).to be > 0.1 if !runs_in_background
20
21
  end
@@ -22,17 +23,16 @@ RSpec.shared_examples "an example valve" do |runs_in_background|
22
23
  it "commands an object" do
23
24
  @person = Employee.start "Alice"
24
25
  @person.promote
25
- @job_title = @person.job_title
26
+ @job_title = await { @person.job_title }
26
27
  expect(@job_title).to eq "Sales manager"
27
28
  end
28
29
  end
29
30
 
30
- RSpec.describe "Valve example: " do
31
+ RSpec.describe "Actor example: " do
31
32
  # standard:disable Lint/ConstantDefinitionInBlock
32
33
  class Employee
33
- include Plumbing::Valve
34
- query :name, :job_title, :greet_slowly
35
- command :promote
34
+ include Plumbing::Actor
35
+ async :name, :job_title, :greet_slowly, :promote
36
36
 
37
37
  def initialize(name)
38
38
  @name = name
@@ -58,7 +58,7 @@ RSpec.describe "Valve example: " do
58
58
  Plumbing.configure mode: :inline, &example
59
59
  end
60
60
 
61
- it_behaves_like "an example valve", false
61
+ it_behaves_like "an example actor", false
62
62
  end
63
63
 
64
64
  context "async mode" do
@@ -68,7 +68,7 @@ RSpec.describe "Valve example: " do
68
68
  end
69
69
  end
70
70
 
71
- it_behaves_like "an example valve", true
71
+ it_behaves_like "an example actor", true
72
72
  end
73
73
 
74
74
  context "threaded mode" do
@@ -76,6 +76,6 @@ RSpec.describe "Valve example: " do
76
76
  Plumbing.configure mode: :threaded, &example
77
77
  end
78
78
 
79
- it_behaves_like "an example valve", true
79
+ it_behaves_like "an example actor", true
80
80
  end
81
81
  end
@@ -6,7 +6,7 @@ RSpec.describe "Pipe examples" do
6
6
  @source = Plumbing::Pipe.start
7
7
 
8
8
  @result = []
9
- @observer = @source.add_observer do |event|
9
+ @source.add_observer do |event|
10
10
  @result << event.type
11
11
  end
12
12
 
@@ -22,7 +22,7 @@ RSpec.describe "Pipe examples" do
22
22
  end
23
23
 
24
24
  @result = []
25
- @observer = @filter.add_observer do |event|
25
+ @filter.add_observer do |event|
26
26
  @result << event.type
27
27
  end
28
28
 
@@ -55,7 +55,7 @@ RSpec.describe "Pipe examples" do
55
55
  @filter = EveryThirdEvent.new(source: @source)
56
56
 
57
57
  @result = []
58
- @observer = @filter.add_observer do |event|
58
+ @filter.add_observer do |event|
59
59
  @result << event.type
60
60
  end
61
61
 
@@ -73,7 +73,7 @@ RSpec.describe "Pipe examples" do
73
73
  @junction = Plumbing::Junction.start @first_source, @second_source
74
74
 
75
75
  @result = []
76
- @observer = @junction.add_observer do |event|
76
+ @junction.add_observer do |event|
77
77
  @result << event.type
78
78
  end
79
79
 
@@ -1,10 +1,12 @@
1
1
  RSpec.shared_examples "a pipe" do
2
2
  it "adds a block observer" do
3
3
  @pipe = described_class.start
4
- @observer = @pipe.add_observer do |event|
5
- puts event.type
4
+ @observer = await do
5
+ @pipe.add_observer do |event|
6
+ puts event.type
7
+ end
6
8
  end
7
- expect(@pipe.is_observer?(@observer)).to eq true
9
+ expect(await { @pipe.is_observer?(@observer) }).to eq true
8
10
  end
9
11
 
10
12
  it "adds a callable observer" do
@@ -13,13 +15,13 @@ RSpec.shared_examples "a pipe" do
13
15
 
14
16
  @pipe.add_observer @proc
15
17
 
16
- expect(@pipe.is_observer?(@proc)).to eq true
18
+ expect(await { @pipe.is_observer?(@proc) }).to eq true
17
19
  end
18
20
 
19
21
  it "does not allow an observer without a #call method" do
20
22
  @pipe = described_class.start
21
23
 
22
- expect { @pipe.add_observer(Object.new) }.to raise_error(TypeError)
24
+ expect { await { @pipe.add_observer(Object.new) } }.to raise_error(TypeError)
23
25
  end
24
26
 
25
27
  it "removes an observer" do
@@ -28,10 +30,10 @@ RSpec.shared_examples "a pipe" do
28
30
 
29
31
  @pipe.remove_observer @proc
30
32
 
31
- expect(@pipe.is_observer?(@proc)).to eq false
33
+ expect(await { @pipe.is_observer?(@proc) }).to eq false
32
34
  end
33
35
 
34
- it "does not send notifications for objects which are not events" do
36
+ it "does not send notifications for objects which are not events" do
35
37
  @pipe = described_class.start
36
38
  @results = []
37
39
  @observer = @pipe.add_observer do |event|
@@ -40,7 +42,7 @@ RSpec.shared_examples "a pipe" do
40
42
 
41
43
  @pipe << Object.new
42
44
 
43
- sleep 0.5
45
+ sleep 0.1
44
46
  expect(@results).to eq []
45
47
  end
46
48
 
@@ -101,6 +103,6 @@ RSpec.shared_examples "a pipe" do
101
103
 
102
104
  @pipe.shutdown
103
105
 
104
- expect(@pipe.is_observer?(@observer)).to eq false
106
+ expect(await { @pipe.is_observer?(@observer) }).to eq false
105
107
  end
106
108
  end
@@ -0,0 +1,158 @@
1
+ require "spec_helper"
2
+ require_relative "../../../lib/plumbing/actor/transporter"
3
+
4
+ RSpec.describe Plumbing::Actor::Transporter do
5
+ # standard:disable Lint/ConstantDefinitionInBlock
6
+ class Record
7
+ include GlobalID::Identification
8
+ attr_reader :id
9
+ def initialize id
10
+ @id = id
11
+ end
12
+
13
+ def == other
14
+ other.id == @id
15
+ end
16
+ end
17
+ # standard:enable Lint/ConstantDefinitionInBlock
18
+ before do
19
+ GlobalID.app = "rspec"
20
+ GlobalID::Locator.use :rspec do |gid, options|
21
+ Record.new gid.model_id
22
+ end
23
+ end
24
+
25
+ context "marshalling" do
26
+ it "passes simple arguments" do
27
+ @transporter = described_class.new
28
+
29
+ @transport = @transporter.marshal "Hello"
30
+ expect(@transport).to eq ["Hello"]
31
+
32
+ @transport = @transporter.marshal 1, 2, 3
33
+ expect(@transport).to eq [1, 2, 3]
34
+ end
35
+
36
+ it "copies arrays" do
37
+ @transporter = described_class.new
38
+
39
+ @source = [[1, 2, 3], [:this, :that]]
40
+
41
+ @transport = @transporter.marshal(*@source)
42
+ expect(@transport).to eq @source
43
+ expect(@transport.first.object_id).to_not eq @source.first.object_id
44
+ expect(@transport.last.object_id).to_not eq @source.last.object_id
45
+ end
46
+
47
+ it "copies hashss" do
48
+ @transporter = described_class.new
49
+
50
+ @source = [{first: "1", second: 2}]
51
+
52
+ @transport = @transporter.marshal(*@source)
53
+ expect(@transport).to eq @source
54
+ expect(@transport.first.object_id).to_not eq @source.first.object_id
55
+ end
56
+
57
+ it "converts objects to Global ID strings" do
58
+ @transporter = described_class.new
59
+
60
+ @record = Record.new 123
61
+ @global_id = @record.to_global_id.to_s
62
+
63
+ @transport = @transporter.marshal @record
64
+
65
+ expect(@transport).to eq [@global_id]
66
+ end
67
+
68
+ it "converts objects within arrays to Global ID strings" do
69
+ @transporter = described_class.new
70
+
71
+ @record = Record.new 123
72
+ @global_id = @record.to_global_id.to_s
73
+
74
+ @transport = @transporter.marshal [:this, @record]
75
+
76
+ expect(@transport).to eq [[:this, @global_id]]
77
+ end
78
+
79
+ it "converts objects within hashes to Global ID strings" do
80
+ @transporter = described_class.new
81
+
82
+ @record = Record.new 123
83
+ @global_id = @record.to_global_id.to_s
84
+
85
+ @transport = @transporter.marshal this: "that", the_other: {embedded: @record}
86
+
87
+ expect(@transport).to eq [{this: "that", the_other: {embedded: @global_id}}]
88
+ end
89
+ end
90
+
91
+ context "unmarshalling" do
92
+ it "passes simple arguments" do
93
+ @transporter = described_class.new
94
+
95
+ @transport = @transporter.unmarshal "Hello"
96
+ expect(@transport).to eq ["Hello"]
97
+
98
+ @transport = @transporter.unmarshal 1, 2, 3
99
+ expect(@transport).to eq [1, 2, 3]
100
+ end
101
+
102
+ it "passes arrays" do
103
+ @transporter = described_class.new
104
+
105
+ @transport = @transporter.unmarshal [1, 2, 3], [:this, :that]
106
+
107
+ expect(@transport.first.object_id).to_not eq [1, 2, 3]
108
+ expect(@transport.last.object_id).to_not eq [:this, :that]
109
+ end
110
+
111
+ it "passes hashss and keyword arguments" do
112
+ @transporter = described_class.new
113
+
114
+ @transport = @transporter.unmarshal first: "1", second: 2
115
+ expect(@transport).to eq [{first: "1", second: 2}]
116
+ end
117
+
118
+ it "passes mixtures of arrays and hashes" do
119
+ @transporter = described_class.new
120
+
121
+ @transport = @transporter.unmarshal :this, :that, first: "1", second: 2
122
+ expect(@transport).to eq [:this, :that, {first: "1", second: 2}]
123
+ end
124
+
125
+ it "converts Global ID strings to objects" do
126
+ @transporter = described_class.new
127
+
128
+ @record = Record.new "123"
129
+ @global_id = @record.to_global_id.to_s
130
+
131
+ @transport = @transporter.unmarshal @global_id
132
+
133
+ expect(@transport).to eq [@record]
134
+ end
135
+
136
+ it "converts Global ID strings within arrays to objects" do
137
+ @transporter = described_class.new
138
+
139
+ @record = Record.new "123"
140
+ @global_id = @record.to_global_id.to_s
141
+
142
+ @transport = @transporter.unmarshal :this, @global_id
143
+
144
+ expect(@transport).to eq [:this, @record]
145
+ end
146
+
147
+ it "converts Global ID strings within hashes to objects" do
148
+ @transporter = described_class.new
149
+
150
+ @record = Record.new "123"
151
+ @global_id = @record.to_global_id.to_s
152
+
153
+ @transport = @transporter.unmarshal this: "that", the_other: {embedded: @global_id}
154
+
155
+ expect(@transport).to eq [{this: "that", the_other: {embedded: @record}}]
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,208 @@
1
+ require "spec_helper"
2
+
3
+ require_relative "../../lib/plumbing/actor/async"
4
+ require_relative "../../lib/plumbing/actor/threaded"
5
+ require_relative "../../lib/plumbing/actor/rails"
6
+
7
+ RSpec.describe Plumbing::Actor do
8
+ # standard:disable Lint/ConstantDefinitionInBlock
9
+ class Counter
10
+ include Plumbing::Actor
11
+ async :name, :count, :slow_query, "slowly_increment", "raises_error"
12
+ attr_reader :name, :count
13
+
14
+ def initialize name, initial_value: 0
15
+ @name = name
16
+ @count = initial_value
17
+ end
18
+
19
+ protected
20
+
21
+ def slowly_increment
22
+ sleep 0.2
23
+ @count += 1
24
+ end
25
+
26
+ def slow_query
27
+ sleep 0.2
28
+ @count
29
+ end
30
+
31
+ def raises_error = raise "I'm an error"
32
+ end
33
+
34
+ class StepCounter < Counter
35
+ async :step_value
36
+ attr_reader :step_value
37
+
38
+ def initialize name, initial_value: 0, step_value: 5
39
+ super(name, initial_value: initial_value)
40
+ @step_value = step_value
41
+ end
42
+
43
+ protected
44
+
45
+ def slowly_increment
46
+ sleep 0.2
47
+ @count += @step_value
48
+ end
49
+
50
+ def failing_query
51
+ raise "I'm a failure"
52
+ end
53
+ end
54
+ # standard:enable Lint/ConstantDefinitionInBlock
55
+
56
+ it "knows which async messages are understood" do
57
+ expect(Counter.async_messages).to eq [:name, :count, :slow_query, :slowly_increment, :raises_error]
58
+ end
59
+
60
+ it "reuses existing proxy classes" do
61
+ @counter = Counter.start "inline counter", initial_value: 100
62
+ @proxy_class = @counter.class
63
+
64
+ @counter = Counter.start "another inline counter", initial_value: 200
65
+ expect(@counter.class).to eq @proxy_class
66
+ end
67
+
68
+ it "includes commands and queries from the superclass" do
69
+ expect(StepCounter.async_messages).to eq [:name, :count, :slow_query, :slowly_increment, :raises_error, :step_value]
70
+
71
+ @step_counter = StepCounter.start "step counter", initial_value: 100, step_value: 10
72
+
73
+ expect(@step_counter.count.await).to eq 100
74
+ expect(@step_counter.step_value.await).to eq 10
75
+ @step_counter.slowly_increment
76
+ expect(@step_counter.count.await).to eq 110
77
+ end
78
+
79
+ context "inline" do
80
+ around :example do |example|
81
+ Plumbing.configure mode: :inline, &example
82
+ end
83
+
84
+ it "returns the result from a message immediately" do
85
+ @counter = Counter.start "inline counter", initial_value: 100
86
+ @time = Time.now
87
+
88
+ expect(@counter.name.await).to eq "inline counter"
89
+ expect(@counter.count.await).to eq 100
90
+ expect(Time.now - @time).to be < 0.1
91
+
92
+ expect(@counter.slow_query.await).to eq 100
93
+ expect(Time.now - @time).to be > 0.1
94
+ end
95
+
96
+ it "sends all commands immediately" do
97
+ @counter = Counter.start "inline counter", initial_value: 100
98
+ @time = Time.now
99
+
100
+ @counter.slowly_increment
101
+
102
+ expect(@counter.count.await).to eq 101
103
+ expect(Time.now - @time).to be > 0.1
104
+ end
105
+ end
106
+
107
+ [:threaded, :async].each do |mode|
108
+ context mode.to_s do
109
+ around :example do |example|
110
+ Sync do
111
+ Plumbing.configure mode: mode, &example
112
+ end
113
+ end
114
+
115
+ it "performs queries in the background and waits for the response" do
116
+ @counter = Counter.start "async counter", initial_value: 100
117
+ @time = Time.now
118
+
119
+ expect(@counter.name.await).to eq "async counter"
120
+ expect(@counter.count.await).to eq 100
121
+ expect(Time.now - @time).to be < 0.1
122
+
123
+ expect(@counter.slow_query.await).to eq 100
124
+ expect(Time.now - @time).to be > 0.1
125
+ end
126
+
127
+ it "performs queries ignoring the response and returning immediately" do
128
+ @counter = Counter.start "threaded counter", initial_value: 100
129
+ @time = Time.now
130
+
131
+ @counter.slow_query
132
+
133
+ expect(Time.now - @time).to be < 0.1
134
+ end
135
+
136
+ it "performs commands in the background and returning immediately" do
137
+ @counter = Counter.start "threaded counter", initial_value: 100
138
+ @time = Time.now
139
+
140
+ @counter.slowly_increment
141
+ expect(Time.now - @time).to be < 0.1
142
+
143
+ # wait for the background task to complete
144
+ expect(101).to become_equal_to { @counter.count.await }
145
+ expect(Time.now - @time).to be > 0.1
146
+ end
147
+
148
+ it "re-raises exceptions when checking the result" do
149
+ @counter = Counter.start "failure"
150
+
151
+ expect { @counter.raises_error.await }.to raise_error "I'm an error"
152
+ end
153
+
154
+ it "does not raise exceptions if ignoring the result" do
155
+ @counter = Counter.start "failure"
156
+
157
+ expect { @counter.raises_error }.not_to raise_error
158
+ end
159
+ end
160
+ end
161
+
162
+ context "threaded" do
163
+ around :example do |example|
164
+ Plumbing.configure mode: :threaded, &example
165
+ end
166
+
167
+ # standard:disable Lint/ConstantDefinitionInBlock
168
+ class Record
169
+ include GlobalID::Identification
170
+ attr_reader :id
171
+ def initialize id
172
+ @id = id
173
+ end
174
+
175
+ def == other
176
+ other.id == @id
177
+ end
178
+ end
179
+
180
+ class Actor
181
+ include Plumbing::Actor
182
+ async :get_object_id, :get_object
183
+
184
+ private def get_object_id(record) = record.object_id
185
+ private def get_object(record) = record
186
+ end
187
+ # standard:enable Lint/ConstantDefinitionInBlock
188
+
189
+ it "packs and unpacks arguments when sending them across threads" do
190
+ @actor = Actor.start
191
+ @record = Record.new "999"
192
+
193
+ @object_id = @actor.get_object_id(@record).await
194
+
195
+ expect(@object_id).to_not eq @record.object_id
196
+ end
197
+
198
+ it "packs and unpacks results when sending them across threads" do
199
+ @actor = Actor.start
200
+ @record = Record.new "999"
201
+
202
+ @object = @actor.get_object(@record).await
203
+
204
+ expect(@object.id).to eq @record.id
205
+ expect(@object.object_id).to_not eq @record.object_id
206
+ end
207
+ end
208
+ end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: standard-procedure-plumbing
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.3
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rahoul Baruah
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-09-14 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2024-09-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: globalid
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  description: A composable event pipeline and sequential pipelines of operations
14
28
  email:
15
29
  - rahoulb@echodek.co
@@ -20,6 +34,13 @@ files:
20
34
  - README.md
21
35
  - Rakefile
22
36
  - lib/plumbing.rb
37
+ - lib/plumbing/actor.rb
38
+ - lib/plumbing/actor/async.rb
39
+ - lib/plumbing/actor/inline.rb
40
+ - lib/plumbing/actor/kernel.rb
41
+ - lib/plumbing/actor/rails.rb
42
+ - lib/plumbing/actor/threaded.rb
43
+ - lib/plumbing/actor/transporter.rb
23
44
  - lib/plumbing/config.rb
24
45
  - lib/plumbing/custom_filter.rb
25
46
  - lib/plumbing/error.rb
@@ -35,26 +56,21 @@ files:
35
56
  - lib/plumbing/rubber_duck/object.rb
36
57
  - lib/plumbing/rubber_duck/proxy.rb
37
58
  - lib/plumbing/types.rb
38
- - lib/plumbing/valve.rb
39
- - lib/plumbing/valve/async.rb
40
- - lib/plumbing/valve/inline.rb
41
- - lib/plumbing/valve/message.rb
42
- - lib/plumbing/valve/rails.rb
43
- - lib/plumbing/valve/threaded.rb
44
59
  - lib/plumbing/version.rb
45
60
  - spec/become_equal_to_matcher.rb
61
+ - spec/examples/actor_spec.rb
46
62
  - spec/examples/pipe_spec.rb
47
63
  - spec/examples/pipeline_spec.rb
48
64
  - spec/examples/rubber_duck_spec.rb
49
- - spec/examples/valve_spec.rb
50
65
  - spec/plumbing/a_pipe.rb
66
+ - spec/plumbing/actor/transporter_spec.rb
67
+ - spec/plumbing/actor_spec.rb
51
68
  - spec/plumbing/custom_filter_spec.rb
52
69
  - spec/plumbing/filter_spec.rb
53
70
  - spec/plumbing/junction_spec.rb
54
71
  - spec/plumbing/pipe_spec.rb
55
72
  - spec/plumbing/pipeline_spec.rb
56
73
  - spec/plumbing/rubber_duck_spec.rb
57
- - spec/plumbing/valve_spec.rb
58
74
  - spec/plumbing_spec.rb
59
75
  - spec/spec_helper.rb
60
76
  homepage: https://github.com/standard-procedure/plumbing
@@ -1,43 +0,0 @@
1
- require "async"
2
- require "async/semaphore"
3
- require "timeout"
4
-
5
- module Plumbing
6
- module Valve
7
- class Async
8
- attr_reader :target
9
-
10
- def initialize target
11
- @target = target
12
- @queue = []
13
- @semaphore = ::Async::Semaphore.new(1)
14
- end
15
-
16
- # Ask the target to answer the given message
17
- def ask(message, *args, **params, &block)
18
- task = @semaphore.async do
19
- @target.send message, *args, **params, &block
20
- end
21
- Timeout.timeout(timeout) do
22
- task.wait
23
- end
24
- end
25
-
26
- # Tell the target to execute the given message
27
- def tell(message, *args, **params, &block)
28
- @semaphore.async do |task|
29
- @target.send message, *args, **params, &block
30
- rescue
31
- nil
32
- end
33
- nil
34
- end
35
-
36
- private
37
-
38
- def timeout
39
- Plumbing.config.timeout
40
- end
41
- end
42
- end
43
- end
@@ -1,20 +0,0 @@
1
- module Plumbing
2
- module Valve
3
- class Inline
4
- def initialize target
5
- @target = target
6
- end
7
-
8
- # Ask the target to answer the given message
9
- def ask(message, ...)
10
- @target.send(message, ...)
11
- end
12
-
13
- # Tell the target to execute the given message
14
- def tell(message, ...)
15
- @target.send(message, ...)
16
- nil
17
- end
18
- end
19
- end
20
- end
@@ -1,5 +0,0 @@
1
- module Plumbing
2
- module Valve
3
- Message = Struct.new :message, :args, :params, :block, :result, :status
4
- end
5
- end