standard-procedure-plumbing 0.3.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -20,4 +20,12 @@ RSpec.describe Plumbing::Pipe do
20
20
 
21
21
  it_behaves_like "a pipe"
22
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
23
31
  end
@@ -5,72 +5,70 @@ RSpec.describe Plumbing::RubberDuck do
5
5
  class Duck
6
6
  def quack = "Quack"
7
7
 
8
- def swim place
9
- "Swim in #{place}"
10
- end
8
+ def swim(place) = "Swim in #{place}"
11
9
 
12
- def fly &block
13
- "Fly #{block.call}"
14
- end
10
+ def fly(&block) = "Fly #{block.call}"
15
11
  end
16
12
  # standard:enable Lint/ConstantDefinitionInBlock
17
13
 
18
- it "verifies that an object matches the RubberDuck type" do
19
- @duck_type = described_class.define :quack, :swim, :fly
20
- @duck = Duck.new
14
+ context "defining rubber ducks" do
15
+ it "verifies that an object matches the RubberDuck type" do
16
+ @duck_type = described_class.define :quack, :swim, :fly
17
+ @duck = Duck.new
21
18
 
22
- expect(@duck_type.verify(@duck)).to eq @duck
23
- end
19
+ expect(@duck_type.verify(@duck)).to eq @duck
20
+ end
24
21
 
25
- it "casts the object to a duck type" do
26
- @duck_type = described_class.define :quack, :swim, :fly
27
- @duck = Duck.new
22
+ it "casts the object to a duck type" do
23
+ @duck_type = described_class.define :quack, :swim, :fly
24
+ @duck = Duck.new
28
25
 
29
- @proxy = @duck.as @duck_type
26
+ @proxy = @duck.as @duck_type
30
27
 
31
- expect(@proxy).to be_kind_of Plumbing::RubberDuck::Proxy
32
- expect(@proxy).to respond_to :quack
33
- expect(@proxy.quack).to eq "Quack"
34
- expect(@proxy).to respond_to :swim
35
- expect(@proxy.swim("the river")).to eq "Swim in the river"
36
- expect(@proxy).to respond_to :fly
37
- expect(@proxy.fly { "ducky fly" }).to eq "Fly ducky fly"
38
- end
28
+ expect(@proxy).to be_kind_of Plumbing::RubberDuck::Proxy
29
+ expect(@proxy).to respond_to :quack
30
+ expect(@proxy.quack).to eq "Quack"
31
+ expect(@proxy).to respond_to :swim
32
+ expect(@proxy.swim("the river")).to eq "Swim in the river"
33
+ expect(@proxy).to respond_to :fly
34
+ expect(@proxy.fly { "ducky fly" }).to eq "Fly ducky fly"
35
+ end
39
36
 
40
- it "does not forward methods that are not part of the duck type" do
41
- @duck_type = described_class.define :swim, :fly
42
- @duck = Duck.new
37
+ it "does not forward methods that are not part of the duck type" do
38
+ @duck_type = described_class.define :swim, :fly
39
+ @duck = Duck.new
43
40
 
44
- @proxy = @duck.as @duck_type
41
+ @proxy = @duck.as @duck_type
45
42
 
46
- expect(@proxy).to_not respond_to :quack
47
- end
43
+ expect(@proxy).to_not respond_to :quack
44
+ end
48
45
 
49
- it "does not wrap rubber ducks in a proxy" do
50
- @duck_type = described_class.define :swim, :fly
51
- @duck = Duck.new
46
+ it "does not wrap rubber ducks in a proxy" do
47
+ @duck_type = described_class.define :swim, :fly
48
+ @duck = Duck.new
52
49
 
53
- @proxy = @duck.as @duck_type
50
+ @proxy = @duck.as @duck_type
54
51
 
55
- expect(@proxy.as(@duck_type)).to eq @proxy
56
- end
52
+ expect(@proxy.as(@duck_type)).to eq @proxy
53
+ end
57
54
 
58
- it "allows rubber ducks to be expanded and cast to other types" do
59
- @quackers = described_class.define :quack
60
- @swimming_bird = described_class.define :swim, :fly
61
- @duck = Duck.new
55
+ it "allows rubber ducks to be expanded and cast to other types" do
56
+ @quackers = described_class.define :quack
57
+ @swimming_bird = described_class.define :swim, :fly
58
+ @duck = Duck.new
62
59
 
63
- @swimmer = @duck.as @swimming_bird
64
- @quacker = @swimmer.as @quackers
60
+ @swimmer = @duck.as @swimming_bird
61
+ @quacker = @swimmer.as @quackers
65
62
 
66
- expect(@swimmer).to respond_to :swim
67
- expect(@quacker).to respond_to :quack
68
- end
63
+ expect(@swimmer).to respond_to :swim
64
+ expect(@quacker).to respond_to :quack
65
+ end
69
66
 
70
- it "raises a TypeError if the class responds to the given methods" do
71
- cow_type = described_class.define :moo, :chew
72
- duck = Duck.new
67
+ it "raises a TypeError if the object does not respond to the given methods" do
68
+ @cow_type = described_class.define :moo, :chew
69
+ @duck = Duck.new
73
70
 
74
- expect { cow_type.verify(duck) }.to raise_error(TypeError)
71
+ expect { @cow_type.verify(@duck) }.to raise_error(TypeError)
72
+ end
75
73
  end
76
74
  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.2
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-13 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
@@ -31,27 +52,25 @@ files:
31
52
  - lib/plumbing/pipeline/contracts.rb
32
53
  - lib/plumbing/pipeline/operations.rb
33
54
  - lib/plumbing/rubber_duck.rb
55
+ - lib/plumbing/rubber_duck/module.rb
34
56
  - lib/plumbing/rubber_duck/object.rb
35
57
  - lib/plumbing/rubber_duck/proxy.rb
36
58
  - lib/plumbing/types.rb
37
- - lib/plumbing/valve.rb
38
- - lib/plumbing/valve/async.rb
39
- - lib/plumbing/valve/inline.rb
40
- - lib/plumbing/valve/message.rb
41
59
  - lib/plumbing/version.rb
42
60
  - spec/become_equal_to_matcher.rb
61
+ - spec/examples/actor_spec.rb
43
62
  - spec/examples/pipe_spec.rb
44
63
  - spec/examples/pipeline_spec.rb
45
64
  - spec/examples/rubber_duck_spec.rb
46
- - spec/examples/valve_spec.rb
47
65
  - spec/plumbing/a_pipe.rb
66
+ - spec/plumbing/actor/transporter_spec.rb
67
+ - spec/plumbing/actor_spec.rb
48
68
  - spec/plumbing/custom_filter_spec.rb
49
69
  - spec/plumbing/filter_spec.rb
50
70
  - spec/plumbing/junction_spec.rb
51
71
  - spec/plumbing/pipe_spec.rb
52
72
  - spec/plumbing/pipeline_spec.rb
53
73
  - spec/plumbing/rubber_duck_spec.rb
54
- - spec/plumbing/valve_spec.rb
55
74
  - spec/plumbing_spec.rb
56
75
  - spec/spec_helper.rb
57
76
  homepage: https://github.com/standard-procedure/plumbing
@@ -76,7 +95,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
76
95
  - !ruby/object:Gem::Version
77
96
  version: '0'
78
97
  requirements: []
79
- rubygems_version: 3.5.17
98
+ rubygems_version: 3.5.12
80
99
  signing_key:
81
100
  specification_version: 4
82
101
  summary: Plumbing - various pipelines for your ruby application
@@ -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
@@ -1,71 +0,0 @@
1
- require_relative "valve/inline"
2
-
3
- module Plumbing
4
- module Valve
5
- def self.included base
6
- base.extend ClassMethods
7
- end
8
-
9
- module ClassMethods
10
- # Create a new valve instance and build a proxy for it using the current mode
11
- # @return [Plumbing::Valve::Base] the proxy for the valve instance
12
- def start(*, **, &)
13
- build_proxy_for(new(*, **, &))
14
- end
15
-
16
- # Define the queries that this valve can answer
17
- # @param names [Array<Symbol>] the names of the queries
18
- def query(*names) = queries.concat(names.map(&:to_sym))
19
-
20
- # List the queries that this valve can answer
21
- def queries = @queries ||= []
22
-
23
- # Define the commands that this valve can execute
24
- # @param names [Array<Symbol>] the names of the commands
25
- def command(*names) = commands.concat(names.map(&:to_sym))
26
-
27
- # List the commands that this valve can execute
28
- def commands = @commands ||= []
29
-
30
- def inherited subclass
31
- subclass.commands.concat commands
32
- subclass.queries.concat queries
33
- end
34
-
35
- private
36
-
37
- def build_proxy_for(target)
38
- proxy_class_for(target.class).new(target)
39
- end
40
-
41
- def proxy_class_for target_class
42
- Plumbing.config.valve_proxy_class_for(target_class) || register_valve_proxy_class_for(target_class)
43
- end
44
-
45
- def proxy_base_class = const_get "Plumbing::Valve::#{Plumbing.config.mode.to_s.capitalize}"
46
-
47
- def register_valve_proxy_class_for target_class
48
- Plumbing.config.register_valve_proxy_class_for(target_class, build_proxy_class)
49
- end
50
-
51
- def build_proxy_class
52
- Class.new(proxy_base_class).tap do |proxy_class|
53
- queries.each do |query|
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
- end
57
- end
58
-
59
- commands.each do |command|
60
- proxy_class.define_method command do |*args, **params, &block|
61
- tell(command, *args, **params, &block)
62
- nil
63
- rescue
64
- nil
65
- end
66
- end
67
- end
68
- end
69
- end
70
- end
71
- end
@@ -1,88 +0,0 @@
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