standard-procedure-plumbing 0.3.2 → 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 54681502a7df050136406e706c4fd6b9d9bffea81e44c151e969379e6803c532
4
- data.tar.gz: ed03cbc9476d43822792fd6a4788b0c2e6c629545056063342f15c0861dc7dd0
3
+ metadata.gz: ba7883be2c006839c549d70a37c96dced0cc0e38af1093676a3503bbbd9dcd95
4
+ data.tar.gz: 9ad82790ba2badcc614a795b559347b6413e810e35fe01db572f1a0f2ccbeb49
5
5
  SHA512:
6
- metadata.gz: 1387fdd83a547157541f444ccf2dcb9465c261616f424dc530ef4c9c13dda52214c21c4d50c99eb825e4827268f4892d62a29d212af9be21bc5bd65dd83422a4
7
- data.tar.gz: c0dcbde3271e4d34cd41d16bda9c488ee40bd838ac0f006fd46bd23eb2c0c1cc4ea18d86236de1d8e87f6b5b97fc5dc728bedbd5a3e354fa09886094866404c9
6
+ metadata.gz: 57f478c6b91598bdc88028ac99bceff692a34d620809ac71b3d89b24930794d8db5d9723b0cde844deaf3b9973bdda3c68ee7c77e1a8c9c824748b5278e5c606
7
+ data.tar.gz: 2dd95476896445746356141cbe5925490d9d7e4b96b9a60aefd0841b8b8851990d235c866bfb4073a8a0b5adb9588af1f88eff43988969ac1a5960ef091483ed
data/README.md CHANGED
@@ -1,55 +1,59 @@
1
1
  # Plumbing
2
2
 
3
- ## Configuration
3
+ ## Configuration
4
4
 
5
- The most important configuration setting is the `mode`, which governs how messages are handled by Valves.
5
+ The most important configuration setting is the `mode`, which governs how messages are handled by Valves.
6
6
 
7
- By default it is `:inline`, so every command or query is handled synchronously.
7
+ By default it is `:inline`, so every command or query is handled synchronously. This is the ruby behaviour you know and love.
8
8
 
9
- If it is set to `:async`, commands and queries will be handled using fibers (via the [Async gem](https://socketry.github.io/async/index.html)).
9
+ If it is set to `:async`, commands and queries will be handled asynchronously using fibers (via the [Async gem](https://socketry.github.io/async/index.html)). Your code should include the "async" gem in its bundle, as Plumbing does not load it by default.
10
10
 
11
- The `timeout` setting is used when performing queries - it defaults to 30s.
11
+ If it is set to `:threaded`, commands and queries will be handled asynchronously by a thread pool (via [Concurrent Ruby](https://ruby-concurrency.github.io/concurrent-ruby/master/Concurrent/Promises.html)), using the default `:io` executor. Your code should include the "concurrent-ruby" gem in its bundle, as Plumbing does not load it by default.
12
+
13
+ If you want to use threads in a Rails application, set the mode to `:rails`. This ensures that the work is wrapped in the Rails executor (which prevents multi-threading issues in the framework). At present, the `:io` executor may cause issues as we may exceed the number of database connections in the Rails' connection pool. We will fix this at some point in the future.
14
+
15
+ The `timeout` setting is used when performing queries - it defaults to 30s.
12
16
 
13
17
  ```ruby
14
18
  require "plumbing"
15
- puts Plumbing.config.mode
19
+ puts Plumbing.config.mode
16
20
  # => :inline
17
21
 
18
22
  Plumbing.configure mode: :async, timeout: 10
19
23
 
20
- puts Plumbing.config.mode
24
+ puts Plumbing.config.mode
21
25
  # => :async
22
26
  ```
23
27
 
24
- If you are running a test suite, you can temporarily update the configuration by passing a block.
28
+ If you are running a test suite, you can temporarily update the configuration by passing a block.
25
29
 
26
30
  ```ruby
27
31
  require "plumbing"
28
- puts Plumbing.config.mode
32
+ puts Plumbing.config.mode
29
33
  # => :inline
30
34
 
31
- Plumbing.configure mode: :async do
32
- puts Plumbing.config.mode
35
+ Plumbing.configure mode: :async do
36
+ puts Plumbing.config.mode
33
37
  # => :async
34
38
  first_test
35
39
  second_test
36
40
  end
37
41
 
38
- puts Plumbing.config.mode
42
+ puts Plumbing.config.mode
39
43
  # => :inline
40
44
  ```
41
45
 
42
46
  ## Plumbing::Pipeline - transform data through a pipeline
43
47
 
44
- Define a sequence of operations that proceed in order, passing their output from one operation as the input to another. [Unix pipes](https://en.wikipedia.org/wiki/Pipeline_(Unix)) in Ruby.
48
+ Define a sequence of operations that proceed in order, passing their output from one operation as the input to another. [Unix pipes](https://en.wikipedia.org/wiki/Pipeline_(Unix)) in Ruby.
45
49
 
46
- Use `perform` to define a step that takes some input and returns a different output.
47
- Specify `using` to re-use an existing `Plumbing::Pipeline` as a step within this pipeline.
48
- Use `execute` to define a step that takes some input, performs an action but passes the input, unchanged, to the next step.
50
+ Use `perform` to define a step that takes some input and returns a different output.
51
+ Specify `using` to re-use an existing `Plumbing::Pipeline` as a step within this pipeline.
52
+ Use `execute` to define a step that takes some input, performs an action but passes the input, unchanged, to the next step.
49
53
 
50
- If you have [dry-validation](https://dry-rb.org/gems/dry-validation/1.10/) installed, you can validate your input using a `Dry::Validation::Contract`. Alternatively, you can define a `pre_condition` to test that the inputs are valid.
54
+ If you have [dry-validation](https://dry-rb.org/gems/dry-validation/1.10/) installed, you can validate your input using a `Dry::Validation::Contract`. Alternatively, you can define a `pre_condition` to test that the inputs are valid.
51
55
 
52
- You can also verify that the output generated is as expected by defining a `post_condition`.
56
+ You can also verify that the output generated is as expected by defining a `post_condition`.
53
57
 
54
58
  ### Usage:
55
59
 
@@ -116,7 +120,7 @@ You can also verify that the output generated is as expected by defining a `post
116
120
  end
117
121
 
118
122
  SayHello.new.call(name: "Alice", email: "alice@example.com")
119
- # => Hello Alice - I will now send a load of annoying marketing messages to alice@example.com
123
+ # => Hello Alice - I will now send a load of annoying marketing messages to alice@example.com
120
124
 
121
125
  SayHello.new.call(some: "other data")
122
126
  # => Plumbing::PreConditionError
@@ -152,30 +156,30 @@ You can also verify that the output generated is as expected by defining a `post
152
156
 
153
157
  ## Plumbing::Valve - safe asynchronous objects
154
158
 
155
- An [actor](https://en.wikipedia.org/wiki/Actor_model) defines the messages an object can receive, similar to a regular object. However, a normal object if accessed concurrently can have data consistency issues and race conditions leading to hard-to-reproduce bugs. Actors, however, ensure that, no matter which thread (or fiber) is sending the message, the internal processing of the message (the method definition) is handled sequentially. This means the internal state of an object is never accessed concurrently, eliminating those issues.
159
+ An [actor](https://en.wikipedia.org/wiki/Actor_model) defines the messages an object can receive, similar to a regular object. However, a normal object if accessed concurrently can have data consistency issues and race conditions leading to hard-to-reproduce bugs. Actors, however, ensure that, no matter which thread (or fiber) is sending the message, the internal processing of the message (the method definition) is handled sequentially. This means the internal state of an object is never accessed concurrently, eliminating those issues.
156
160
 
157
- [Plumbing::Valve](/lib/plumbing/valve.rb) ensures that all messages received are channelled into a concurrency-safe queue. This allows you to take an existing class and ensures that messages received via its public API are made concurrency-safe.
161
+ [Plumbing::Valve](/lib/plumbing/valve.rb) ensures that all messages received are channelled into a concurrency-safe queue. This allows you to take an existing class and ensures that messages received via its public API are made concurrency-safe.
158
162
 
159
- Include the Plumbing::Valve module into your class, define the messages the objects can respond to and set the `Plumbing` configuration to set the desired concurrency model. Messages themselves are split into two categories: commands and queries.
163
+ Include the Plumbing::Valve module into your class, define the messages the objects can respond to and set the `Plumbing` configuration to set the desired concurrency model. Messages themselves are split into two categories: commands and queries.
160
164
 
161
165
  - Commands have no return value so when the message is sent, the caller does not block, the task is called asynchronously and the caller continues immediately
162
166
  - Queries return a value so the caller blocks until the actor has returned a value
163
167
  - However, if you call a query and pass `ignore_result: true` then the query will not block, although you will not be able to access the return value - this is for commands that do something and then return a result based on that work (which you may or may not be interested in - see Plumbing::Pipe#add_observer)
164
168
  - None of the above applies if the `Plumbing mode` is set to `:inline` (which is the default) - in this case, the actor behaves like normal ruby code
165
169
 
166
- Instead of constructing your object with `.new`, use `.start`. This builds a proxy object that wraps the target instance and dispatches messages through a safe mechanism. Only messages that have been defined as part of the valve are available in this proxy - so you don't have to worry about callers bypassing the valve's internal context.
170
+ Instead of constructing your object with `.new`, use `.start`. This builds a proxy object that wraps the target instance and dispatches messages through a safe mechanism. Only messages that have been defined as part of the valve are available in this proxy - so you don't have to worry about callers bypassing the valve's internal context.
167
171
 
168
- Even when using actors, there is one condition where concurrency may cause issues. If object A makes a query to object B which in turn makes a query back to object A, you will hit a deadlock. This is because A is waiting on the response from B but B is now querying, and waiting for, A. This does not apply to commands because they do not wait for a response. However, when writing queries, be careful who you interact with - the configuration allows you to set a timeout (defaulting to 30s) in case this happens.
172
+ Even when using actors, there is one condition where concurrency may cause issues. If object A makes a query to object B which in turn makes a query back to object A, you will hit a deadlock. This is because A is waiting on the response from B but B is now querying, and waiting for, A. This does not apply to commands because they do not wait for a response. However, when writing queries, be careful who you interact with - the configuration allows you to set a timeout (defaulting to 30s) in case this happens.
169
173
 
170
- Also be aware that if you use valves in one place, you need to use them everywhere - especially if you're using threads or ractors (coming soon). This is because as the valve sends messages to its collaborators, those calls will be made from within the valve's internal context. If the collaborators are also valves, the subsequent messages will be handled correctly, if not, data consistency bugs could occur.
174
+ Also be aware that if you use valves in one place, you need to use them everywhere - especially if you're using threads or ractors (coming soon). This is because as the valve sends messages to its collaborators, those calls will be made from within the valve's internal context. If the collaborators are also valves, the subsequent messages will be handled correctly, if not, data consistency bugs could occur.
171
175
 
172
- ### Usage
176
+ ### Usage
173
177
 
174
178
  [Defining an actor](/spec/examples/valve_spec.rb)
175
179
 
176
180
  ```ruby
177
181
  require "plumbing"
178
-
182
+
179
183
  class Employee
180
184
  attr_reader :name, :job_title
181
185
 
@@ -193,7 +197,7 @@ Also be aware that if you use valves in one place, you need to use them everywhe
193
197
  @job_title = "Sales manager"
194
198
  end
195
199
 
196
- def greet_slowly
200
+ def greet_slowly
197
201
  sleep 0.2
198
202
  "H E L L O"
199
203
  end
@@ -204,7 +208,7 @@ Also be aware that if you use valves in one place, you need to use them everywhe
204
208
 
205
209
  ```ruby
206
210
  require "plumbing"
207
-
211
+
208
212
  @person = Employee.start "Alice"
209
213
 
210
214
  puts @person.name
@@ -217,7 +221,7 @@ Also be aware that if you use valves in one place, you need to use them everywhe
217
221
  puts @person.job_title
218
222
  # => "Sales manager"
219
223
 
220
- @person.greet_slowly
224
+ @person.greet_slowly
221
225
  # this will block for 0.2 seconds before returning "H E L L O"
222
226
 
223
227
  @person.greet_slowly(ignore_result: true)
@@ -230,7 +234,7 @@ Also be aware that if you use valves in one place, you need to use them everywhe
230
234
  require "plumbing"
231
235
  require "async"
232
236
 
233
- Plumbing.configure mode: :async
237
+ Plumbing.configure mode: :async
234
238
  @person = Employee.start "Alice"
235
239
 
236
240
  puts @person.name
@@ -243,20 +247,47 @@ Also be aware that if you use valves in one place, you need to use them everywhe
243
247
  puts @person.job_title
244
248
  # => "Sales manager" (this will block for 0.5s because #job_title query will not start until the #promote command has completed)
245
249
 
246
- @person.greet_slowly
250
+ @person.greet_slowly
247
251
  # this will block for 0.2 seconds before returning "H E L L O"
248
252
 
249
253
  @person.greet_slowly(ignore_result: true)
250
254
  # this will not block and returns nil
251
255
  ```
252
256
 
257
+ [Using threads](/spec/examples/valve_spec.rb) with concurrency and some parallelism
258
+
259
+ ```ruby
260
+ require "plumbing"
261
+ require "concurrent"
262
+
263
+ Plumbing.configure mode: :threaded
264
+ @person = Employee.start "Alice"
265
+
266
+ puts @person.name
267
+ # => "Alice"
268
+ puts @person.job_title
269
+ # => "Sales assistant"
270
+
271
+ @person.promote
272
+ # this will return immediately without blocking
273
+ puts @person.job_title
274
+ # => "Sales manager" (this will block for 0.5s because #job_title query will not start until the #promote command has completed)
275
+
276
+ @person.greet_slowly
277
+ # this will block for 0.2 seconds before returning "H E L L O"
278
+
279
+ @person.greet_slowly(ignore_result: true)
280
+ # this will not block and returns nil
281
+ ```
282
+
283
+
253
284
  ## Plumbing::Pipe - a composable observer
254
285
 
255
286
  [Observers](https://ruby-doc.org/3.3.0/stdlibs/observer/Observable.html) in Ruby are a pattern where objects (observers) register their interest in another object (the observable). This pattern is common throughout programming languages (event listeners in Javascript, the dependency protocol in [Smalltalk](https://en.wikipedia.org/wiki/Smalltalk)).
256
287
 
257
- [Plumbing::Pipe](lib/plumbing/pipe.rb) makes observers "composable". Instead of simply just registering for notifications from a single observable, we can build sequences of pipes. These sequences can filter notifications and route them to different listeners, or merge multiple sources into a single stream of notifications.
288
+ [Plumbing::Pipe](lib/plumbing/pipe.rb) makes observers "composable". Instead of simply just registering for notifications from a single observable, we can build sequences of pipes. These sequences can filter notifications and route them to different listeners, or merge multiple sources into a single stream of notifications.
258
289
 
259
- Pipes are implemented as valves, meaning that event notifications can be dispatched asynchronously. The observer's callback will be triggered from within the pipe's internal context so you should immediately trigger a command on another valve to maintain safety.
290
+ Pipes are implemented as valves, meaning that event notifications can be dispatched asynchronously. The observer's callback will be triggered from within the pipe's internal context so you should immediately trigger a command on another valve to maintain safety.
260
291
 
261
292
  ### Usage
262
293
 
@@ -279,7 +310,7 @@ Pipes are implemented as valves, meaning that event notifications can be dispatc
279
310
 
280
311
  @source = Plumbing::Pipe.start
281
312
  @filter = Plumbing::Filter.start source: @source do |event|
282
- %w[important urgent].include? event.type
313
+ %w[important urgent].include? event.type
283
314
  end
284
315
  @observer = @filter.add_observer do |event|
285
316
  puts event.type
@@ -349,16 +380,16 @@ Pipes are implemented as valves, meaning that event notifications can be dispatc
349
380
  require "plumbing"
350
381
  require "async"
351
382
 
352
- Plumbing.configure mode: :async
383
+ Plumbing.configure mode: :async
353
384
 
354
- Sync do
355
- @first_source = Plumbing::Pipe.start
385
+ Sync do
386
+ @first_source = Plumbing::Pipe.start
356
387
  @second_source = Plumbing::Pipe.start
357
388
 
358
389
  @junction = Plumbing::Junction.start @first_source, @second_source
359
390
 
360
391
  @filter = Plumbing::Filter.start source: @junction do |event|
361
- %w[one-one two-two].include? event.type
392
+ %w[one-one two-two].include? event.type
362
393
  end
363
394
 
364
395
  @first_source.notify "one-one"
@@ -370,19 +401,20 @@ Pipes are implemented as valves, meaning that event notifications can be dispatc
370
401
 
371
402
  ## Plumbing::RubberDuck - duck types and type-casts
372
403
 
373
- Define an [interface or protocol](https://en.wikipedia.org/wiki/Interface_(object-oriented_programming)) specifying which messages you expect to be able to send. Then cast an object into that type, which first tests that the object can respond to those messages and then builds a proxy that responds to just those messages and no others (so no-one can abuse the specific type-casting you have specified). However, if you take one of these proxies, you can safely re-cast it as another type (as long as the original target object is castable).
404
+ Define an [interface or protocol](https://en.wikipedia.org/wiki/Interface_(object-oriented_programming)) specifying which messages you expect to be able to send.
374
405
 
406
+ Then cast an object into that type. This first tests that the object can respond to those messages and then builds a proxy that responds to those messages (and no others). However, if you take one of these proxies, you can safely re-cast it as another type (as long as the original target object responds to the correct messages).
375
407
 
376
- ### Usage
408
+ ### Usage
377
409
 
378
- Define your interface (Person in this example), then cast your objects (instances of PersonData and CarData).
410
+ Define your interface (Person in this example), then cast your objects (instances of PersonData and CarData).
379
411
 
380
412
  [Casting objects as duck-types](/spec/examples/rubber_duck_spec.rb):
381
413
  ```ruby
382
414
  require "plumbing"
383
415
 
384
- Person = Plumbing::RubberDuck.define :first_name, :last_name, :email
385
- LikesFood = Plumbing::RubberDuck.define :favourite_food
416
+ Person = Plumbing::RubberDuck.define :first_name, :last_name, :email
417
+ LikesFood = Plumbing::RubberDuck.define :favourite_food
386
418
 
387
419
  PersonData = Struct.new(:first_name, :last_name, :email, :favourite_food)
388
420
  CarData = Struct.new(:make, :model, :colour)
@@ -395,17 +427,54 @@ Define your interface (Person in this example), then cast your objects (instance
395
427
  @person = @alice.as Person
396
428
  @person.first_name
397
429
  # => "Alice"
398
- @person.email
430
+ @person.email
399
431
  # => "alice@example.com"
400
432
  @person.favourite_food
401
433
  # => NoMethodError - #favourite_food is not part of the Person rubber duck (even though it is part of the underlying PersonData struct)
402
434
 
403
435
  # Cast our Person into a LikesFood rubber duck
404
- @hungry = @person.as LikesFood
405
- @hungry.favourite_food
436
+ @hungry = @person.as LikesFood
437
+ @hungry.favourite_food
406
438
  # => "Ice cream"
407
439
  ```
408
440
 
441
+ You can also use the same `@object.as type` to type-check instances against modules or classes. This creates a RubberDuck proxy based on the module or class you're casting into. So the cast will pass if the object responds to the correct messages, even if a strict `.is_a?` test would fail.
442
+
443
+ ```ruby
444
+ require "plumbing"
445
+
446
+ module Person
447
+ def first_name = @first_name
448
+
449
+ def last_name = @last_name
450
+
451
+ def email = @email
452
+ end
453
+
454
+ module LikesFood
455
+ def favourite_food = @favourite_food
456
+ end
457
+
458
+ PersonData = Struct.new(:first_name, :last_name, :email, :favourite_food)
459
+ CarData = Struct.new(:make, :model, :colour)
460
+
461
+ @porsche_911 = CarData.new "Porsche", "911", "black"
462
+ expect { @porsche_911.as Person }.to raise_error(TypeError)
463
+
464
+ @alice = PersonData.new "Alice", "Aardvark", "alice@example.com", "Ice cream"
465
+
466
+ @alics.is_a? Person
467
+ # => false - PersonData does not `include Person`
468
+ @person = @alice.as Person
469
+ # This cast is OK because PersonData responds to :first_name, :last_name and :email
470
+ expect(@person.first_name).to eq "Alice"
471
+ expect(@person.email).to eq "alice@example.com"
472
+ expect { @person.favourite_food }.to raise_error(NoMethodError)
473
+
474
+ @hungry = @person.as LikesFood
475
+ expect(@hungry.favourite_food).to eq "Ice cream"
476
+ ```
477
+
409
478
  ## Installation
410
479
 
411
480
  Install the gem and add to the application's Gemfile by executing:
@@ -418,6 +487,9 @@ Then:
418
487
 
419
488
  ```ruby
420
489
  require 'plumbing'
490
+
491
+ # Set the mode for your Valves and Pipes
492
+ Plumbing.config mode: :async
421
493
  ```
422
494
 
423
495
  ## Development
@@ -0,0 +1,13 @@
1
+ module Plumbing
2
+ class RubberDuck
3
+ ::Module.class_eval do
4
+ def rubber_duck
5
+ @rubber_duck ||= Plumbing::RubberDuck.define(*instance_methods)
6
+ end
7
+
8
+ def proxy_for object
9
+ rubber_duck.proxy_for object
10
+ end
11
+ end
12
+ end
13
+ end
@@ -2,9 +2,10 @@ module Plumbing
2
2
  class RubberDuck
3
3
  ::Object.class_eval do
4
4
  # Cast the object to a duck-type
5
+ # @param type [Plumbing::RubberDuck, Module]
5
6
  # @return [Plumbing::RubberDuck::Proxy] the duck-type proxy
6
- def as duck_type
7
- duck_type.proxy_for self
7
+ def as type
8
+ Plumbing::RubberDuck.cast self, type: type
8
9
  end
9
10
  end
10
11
  end
@@ -1,6 +1,7 @@
1
1
  module Plumbing
2
2
  # A type-checker for duck-types
3
3
  class RubberDuck
4
+ require_relative "rubber_duck/module"
4
5
  require_relative "rubber_duck/object"
5
6
  require_relative "rubber_duck/proxy"
6
7
 
@@ -26,10 +27,19 @@ module Plumbing
26
27
  is_a_proxy?(object) || build_proxy_for(object)
27
28
  end
28
29
 
29
- # Define a new rubber duck type
30
- # @param *methods [Array<Symbol>] the methods that the duck-type should respond to
31
- def self.define *methods
32
- new(*methods)
30
+ class << self
31
+ # Define a new rubber duck type
32
+ # @param *methods [Array<Symbol>] the methods that the duck-type should respond to
33
+ def define *methods
34
+ new(*methods)
35
+ end
36
+
37
+ # Cast the object to the given type
38
+ # @param object [Object] to be csat
39
+ # @param to [Module, Plumbing::RubberDuck] the type to cast into
40
+ def cast object, type:
41
+ type.proxy_for object
42
+ end
33
43
  end
34
44
 
35
45
  private
@@ -0,0 +1,15 @@
1
+ require_relative "threaded"
2
+
3
+ module Plumbing
4
+ module Valve
5
+ class Rails < Threaded
6
+ protected
7
+
8
+ def future(&)
9
+ Concurrent::Promises.future do
10
+ Rails.application.executor.wrap(&)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,67 @@
1
+ require "concurrent/array"
2
+ require "concurrent/mvar"
3
+ require "concurrent/immutable_struct"
4
+ require "concurrent/promises"
5
+
6
+ module Plumbing
7
+ module Valve
8
+ class Threaded
9
+ attr_reader :target
10
+
11
+ def initialize target
12
+ @target = target
13
+ @queue = Concurrent::Array.new
14
+ end
15
+
16
+ # Ask the target to answer the given message
17
+ def ask(message, *, **, &)
18
+ add_message_to_queue(message, *, **, &).value
19
+ end
20
+
21
+ # Tell the target to execute the given message
22
+ def tell(message, *, **, &)
23
+ add_message_to_queue(message, *, **, &)
24
+ nil
25
+ rescue
26
+ nil
27
+ end
28
+
29
+ protected
30
+
31
+ def future(&)
32
+ Concurrent::Promises.future(&)
33
+ end
34
+
35
+ private
36
+
37
+ def send_messages
38
+ future do
39
+ while (message = @queue.shift)
40
+ message.call
41
+ end
42
+ end
43
+ end
44
+
45
+ def add_message_to_queue message_name, *args, **params, &block
46
+ Message.new(@target, message_name, args, params, block, Concurrent::MVar.new).tap do |message|
47
+ @queue << message
48
+ send_messages if @queue.size == 1
49
+ end
50
+ end
51
+
52
+ class Message < Concurrent::ImmutableStruct.new(:target, :name, :args, :params, :block, :result)
53
+ def value
54
+ result.take(Plumbing.config.timeout).tap do |value|
55
+ raise value if value.is_a? Exception
56
+ end
57
+ end
58
+
59
+ def call
60
+ result.put target.send(name, *args, **params, &block)
61
+ rescue => ex
62
+ result.put ex
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Plumbing
4
- VERSION = "0.3.2"
4
+ VERSION = "0.3.3"
5
5
  end
@@ -1,26 +1,109 @@
1
1
  require "spec_helper"
2
2
 
3
3
  RSpec.describe "Rubber Duck examples" do
4
- it "casts objects as duck types" do
4
+ it "casts objects into duck types" do
5
5
  # standard:disable Lint/ConstantDefinitionInBlock
6
- Person = Plumbing::RubberDuck.define :first_name, :last_name, :email
7
- LikesFood = Plumbing::RubberDuck.define :favourite_food
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
8
13
 
9
- PersonData = Struct.new(:first_name, :last_name, :email, :favourite_food)
10
- CarData = Struct.new(:make, :model, :colour)
11
14
  # standard:enable Lint/ConstantDefinitionInBlock
12
15
 
13
- @porsche_911 = CarData.new "Porsche", "911", "black"
14
- expect { @porsche_911.as Person }.to raise_error(TypeError)
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)
15
98
 
16
- @alice = PersonData.new "Alice", "Aardvark", "alice@example.com", "Ice cream"
99
+ @alice = ClassExample::PersonWhoLikesFood.new "Alice", "Aardvark", "alice@example.com", "Ice cream"
17
100
 
18
- @person = @alice.as Person
101
+ @person = @alice.as ClassExample::Person
19
102
  expect(@person.first_name).to eq "Alice"
20
103
  expect(@person.email).to eq "alice@example.com"
21
104
  expect { @person.favourite_food }.to raise_error(NoMethodError)
22
105
 
23
- @hungry = @person.as LikesFood
106
+ @hungry = @person.as ClassExample::PersonWhoLikesFood
24
107
  expect(@hungry.favourite_food).to eq "Ice cream"
25
108
  end
26
109
  end
@@ -1,6 +1,33 @@
1
1
  require "spec_helper"
2
2
 
3
- RSpec.describe "Valve examples" do
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
4
31
  # standard:disable Lint/ConstantDefinitionInBlock
5
32
  class Employee
6
33
  include Plumbing::Valve
@@ -26,63 +53,29 @@ RSpec.describe "Valve examples" do
26
53
  end
27
54
  # standard:enable Lint/ConstantDefinitionInBlock
28
55
 
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
56
+ context "inline mode" do
57
+ around :example do |example|
58
+ Plumbing.configure mode: :inline, &example
45
59
  end
46
60
 
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
61
+ it_behaves_like "an example valve", false
56
62
  end
57
63
 
58
- context "async" do
64
+ context "async mode" do
59
65
  around :example do |example|
60
66
  Plumbing.configure mode: :async do
61
67
  Kernel::Async(&example)
62
68
  end
63
69
  end
64
70
 
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
71
+ it_behaves_like "an example valve", true
72
+ end
74
73
 
75
- @time = Time.now
76
- expect(@person.greet_slowly(ignore_result: true)).to be_nil
77
- expect(Time.now - @time).to be < 0.1
74
+ context "threaded mode" do
75
+ around :example do |example|
76
+ Plumbing.configure mode: :threaded, &example
78
77
  end
79
78
 
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
79
+ it_behaves_like "an example valve", true
87
80
  end
88
81
  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
@@ -1,6 +1,8 @@
1
1
  require "spec_helper"
2
- require "async"
2
+
3
3
  require_relative "../../lib/plumbing/valve/async"
4
+ require_relative "../../lib/plumbing/valve/threaded"
5
+ require_relative "../../lib/plumbing/valve/rails"
4
6
 
5
7
  RSpec.describe Plumbing::Valve do
6
8
  # standard:disable Lint/ConstantDefinitionInBlock
@@ -124,44 +126,46 @@ RSpec.describe Plumbing::Valve do
124
126
  end
125
127
  end
126
128
 
127
- context "async" do
128
- around :example do |example|
129
- Sync do
130
- Plumbing.configure mode: :async, &example
129
+ [:threaded, :async].each do |mode|
130
+ context mode.to_s do
131
+ around :example do |example|
132
+ Sync do
133
+ Plumbing.configure mode: mode, &example
134
+ end
131
135
  end
132
- end
133
136
 
134
- it "sends all queries using fibers and waits for the response" do
135
- @counter = Counter.start "async counter", initial_value: 100
136
- @time = Time.now
137
+ it "performs queries in the background and waits for the response" do
138
+ @counter = Counter.start "async counter", initial_value: 100
139
+ @time = Time.now
137
140
 
138
- expect(@counter.name).to eq "async counter"
139
- expect(@counter.count).to eq 100
140
- expect(Time.now - @time).to be < 0.1
141
+ expect(@counter.name).to eq "async counter"
142
+ expect(@counter.count).to eq 100
143
+ expect(Time.now - @time).to be < 0.1
141
144
 
142
- expect(@counter.slow_query).to eq 100
143
- expect(Time.now - @time).to be > 0.4
144
- end
145
+ expect(@counter.slow_query).to eq 100
146
+ expect(Time.now - @time).to be > 0.4
147
+ end
145
148
 
146
- it "ignores the response from a query and returns immediately" do
147
- @counter = Counter.start "async counter", initial_value: 100
148
- @time = Time.now
149
+ it "performs queries ignoring the response and returning immediately" do
150
+ @counter = Counter.start "threaded counter", initial_value: 100
151
+ @time = Time.now
149
152
 
150
- expect(@counter.slow_query(ignore_result: true)).to be_nil
153
+ expect(@counter.slow_query(ignore_result: true)).to be_nil
151
154
 
152
- expect(Time.now - @time).to be < 0.1
153
- end
155
+ expect(Time.now - @time).to be < 0.1
156
+ end
154
157
 
155
- it "sends all commands using fibers without waiting for the response" do
156
- @counter = Counter.start "async counter", initial_value: 100
157
- @time = Time.now
158
+ it "performs commands in the background and returning immediately" do
159
+ @counter = Counter.start "threaded counter", initial_value: 100
160
+ @time = Time.now
158
161
 
159
- @counter.slowly_increment
160
- expect(Time.now - @time).to be < 0.1
162
+ @counter.slowly_increment
163
+ expect(Time.now - @time).to be < 0.1
161
164
 
162
- # wait for the async task to complete
163
- expect(@counter.count).to become_equal_to { 101 }
164
- expect(Time.now - @time).to be > 0.4
165
+ # wait for the threaded task to complete
166
+ expect(101).to become_equal_to { @counter.count }
167
+ expect(Time.now - @time).to be > 0.4
168
+ end
165
169
  end
166
170
  end
167
171
  end
metadata CHANGED
@@ -1,14 +1,14 @@
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.3.3
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
11
+ date: 2024-09-14 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: A composable event pipeline and sequential pipelines of operations
14
14
  email:
@@ -31,6 +31,7 @@ files:
31
31
  - lib/plumbing/pipeline/contracts.rb
32
32
  - lib/plumbing/pipeline/operations.rb
33
33
  - lib/plumbing/rubber_duck.rb
34
+ - lib/plumbing/rubber_duck/module.rb
34
35
  - lib/plumbing/rubber_duck/object.rb
35
36
  - lib/plumbing/rubber_duck/proxy.rb
36
37
  - lib/plumbing/types.rb
@@ -38,6 +39,8 @@ files:
38
39
  - lib/plumbing/valve/async.rb
39
40
  - lib/plumbing/valve/inline.rb
40
41
  - lib/plumbing/valve/message.rb
42
+ - lib/plumbing/valve/rails.rb
43
+ - lib/plumbing/valve/threaded.rb
41
44
  - lib/plumbing/version.rb
42
45
  - spec/become_equal_to_matcher.rb
43
46
  - spec/examples/pipe_spec.rb
@@ -76,7 +79,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
76
79
  - !ruby/object:Gem::Version
77
80
  version: '0'
78
81
  requirements: []
79
- rubygems_version: 3.5.17
82
+ rubygems_version: 3.5.12
80
83
  signing_key:
81
84
  specification_version: 4
82
85
  summary: Plumbing - various pipelines for your ruby application