standard-procedure-plumbing 0.3.2 → 0.4.0

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.
@@ -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