standard-procedure-plumbing 0.3.1 → 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +119 -47
  3. data/lib/plumbing/rubber_duck/module.rb +13 -0
  4. data/lib/plumbing/rubber_duck/object.rb +3 -2
  5. data/lib/plumbing/rubber_duck.rb +14 -4
  6. data/lib/plumbing/valve/rails.rb +15 -0
  7. data/lib/plumbing/valve/threaded.rb +67 -0
  8. data/lib/plumbing/version.rb +1 -1
  9. data/spec/become_equal_to_matcher.rb +25 -0
  10. data/spec/examples/pipe_spec.rb +109 -0
  11. data/spec/examples/pipeline_spec.rb +89 -0
  12. data/spec/examples/rubber_duck_spec.rb +109 -0
  13. data/spec/examples/valve_spec.rb +81 -0
  14. data/spec/plumbing/a_pipe.rb +106 -0
  15. data/spec/plumbing/custom_filter_spec.rb +29 -0
  16. data/spec/plumbing/filter_spec.rb +32 -0
  17. data/spec/plumbing/junction_spec.rb +67 -0
  18. data/spec/plumbing/pipe_spec.rb +31 -0
  19. data/spec/plumbing/pipeline_spec.rb +208 -0
  20. data/spec/plumbing/rubber_duck_spec.rb +74 -0
  21. data/spec/plumbing/valve_spec.rb +171 -0
  22. data/spec/plumbing_spec.rb +7 -0
  23. data/spec/spec_helper.rb +16 -0
  24. metadata +21 -19
  25. data/.rspec +0 -3
  26. data/.rubocop.yml +0 -24
  27. data/.solargraph.yml +0 -32
  28. data/.standard.yml +0 -9
  29. data/.vscode/tasks.json +0 -11
  30. data/CHANGELOG.md +0 -40
  31. data/CODE_OF_CONDUCT.md +0 -5
  32. data/LICENSE +0 -504
  33. data/checksums/standard-procedure-plumbing-0.1.1.gem.sha512 +0 -1
  34. data/checksums/standard-procedure-plumbing-0.1.2.gem.sha512 +0 -1
  35. data/checksums/standard-procedure-plumbing-0.2.0.gem.sha512 +0 -1
  36. data/checksums/standard-procedure-plumbing-0.2.1.gem.sha512 +0 -1
  37. data/checksums/standard-procedure-plumbing-0.2.2.gem.sha512 +0 -1
  38. data/checksums/standard-procedure-plumbing-0.3.0.gem.sha512 +0 -1
  39. data/checksums/standard-procedure-plumbing-0.3.1.gem.sha512 +0 -1
  40. data/sig/plumbing.rbs +0 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9a111ce599942b5e33b6928cede476b75e56fd6574625bf50ca82d6d75f3e718
4
- data.tar.gz: b1b4c48062aec9d77ec3b917c25723fae8ff2cc4075d9f91c699da3f165b7c84
3
+ metadata.gz: ba7883be2c006839c549d70a37c96dced0cc0e38af1093676a3503bbbd9dcd95
4
+ data.tar.gz: 9ad82790ba2badcc614a795b559347b6413e810e35fe01db572f1a0f2ccbeb49
5
5
  SHA512:
6
- metadata.gz: 6d2de706d57ef380e67fcd9d79b3d202ca0741f7ed24552ad62bc63944f1c0a82cdd6123560a6d79a85099218db5c9c966ee602a0ef281ea7d5b0305e1f5a707
7
- data.tar.gz: 47de86307b817c0d0399bc06bcac5f78eea3629b93725b785f1dc6a442a300a03ceadde6627f2d198ea296524f584f29a90fa3ec52e5e635507c024241fec5da
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.1"
4
+ VERSION = "0.3.3"
5
5
  end
@@ -0,0 +1,25 @@
1
+ require "rspec/expectations"
2
+
3
+ # Custom matcher that repeatedly evaluates the block until it matches the expected value or 5 seconds have elapsed
4
+ #
5
+ # This allows asynchronous operations to be tested in a synchronous manner with a timeout
6
+ #
7
+ # Example:
8
+ # expect("Hello").to become_equal_to { subject.greeting }
9
+ #
10
+ RSpec::Matchers.define :become_equal_to do
11
+ match do |expected|
12
+ counter = 0
13
+ matched = false
14
+ while (counter < 50) && (matched == false)
15
+ matched = true if (@result = block_arg.call) == expected
16
+ sleep 0.1
17
+ counter += 1
18
+ end
19
+ matched
20
+ end
21
+
22
+ failure_message do |expected|
23
+ "expected block to return #{expected} but was #{@result} after timeout expired"
24
+ end
25
+ end
@@ -0,0 +1,109 @@
1
+ require "spec_helper"
2
+ require "async"
3
+
4
+ RSpec.describe "Pipe examples" do
5
+ it "observes events" do
6
+ @source = Plumbing::Pipe.start
7
+
8
+ @result = []
9
+ @observer = @source.add_observer do |event|
10
+ @result << event.type
11
+ end
12
+
13
+ @source.notify "something_happened", message: "But what was it?"
14
+ expect(@result).to eq ["something_happened"]
15
+ end
16
+
17
+ it "filters events" do
18
+ @source = Plumbing::Pipe.start
19
+
20
+ @filter = Plumbing::Filter.start source: @source do |event|
21
+ %w[important urgent].include? event.type
22
+ end
23
+
24
+ @result = []
25
+ @observer = @filter.add_observer do |event|
26
+ @result << event.type
27
+ end
28
+
29
+ @source.notify "important", message: "ALERT! ALERT!"
30
+ expect(@result).to eq ["important"]
31
+
32
+ @source.notify "unimportant", message: "Nothing to see here"
33
+ expect(@result).to eq ["important"]
34
+ end
35
+
36
+ it "allows for custom filters" do
37
+ # standard:disable Lint/ConstantDefinitionInBlock
38
+ class EveryThirdEvent < Plumbing::CustomFilter
39
+ def initialize source:
40
+ super
41
+ @events = []
42
+ end
43
+
44
+ def received event
45
+ @events << event
46
+ if @events.count >= 3
47
+ @events.clear
48
+ self << event
49
+ end
50
+ end
51
+ end
52
+ # standard:enable Lint/ConstantDefinitionInBlock
53
+
54
+ @source = Plumbing::Pipe.start
55
+ @filter = EveryThirdEvent.new(source: @source)
56
+
57
+ @result = []
58
+ @observer = @filter.add_observer do |event|
59
+ @result << event.type
60
+ end
61
+
62
+ 1.upto 10 do |i|
63
+ @source.notify i.to_s
64
+ end
65
+
66
+ expect(@result).to eq ["3", "6", "9"]
67
+ end
68
+
69
+ it "joins multiple source pipes" do
70
+ @first_source = Plumbing::Pipe.start
71
+ @second_source = Plumbing::Pipe.start
72
+
73
+ @junction = Plumbing::Junction.start @first_source, @second_source
74
+
75
+ @result = []
76
+ @observer = @junction.add_observer do |event|
77
+ @result << event.type
78
+ end
79
+
80
+ @first_source.notify "one"
81
+ expect(@result).to eq ["one"]
82
+ @second_source.notify "two"
83
+ expect(@result).to eq ["one", "two"]
84
+ end
85
+
86
+ it "dispatches events asynchronously using fibers" do
87
+ Plumbing.configure mode: :async do
88
+ Sync do
89
+ @first_source = Plumbing::Pipe.start
90
+ @second_source = Plumbing::Pipe.start
91
+ @junction = Plumbing::Junction.start @first_source, @second_source
92
+ @filter = Plumbing::Filter.start source: @junction do |event|
93
+ %w[one-one two-two].include? event.type
94
+ end
95
+ @result = []
96
+ @filter.add_observer do |event|
97
+ @result << event.type
98
+ end
99
+
100
+ @first_source.notify "one-one"
101
+ @first_source.notify "one-two"
102
+ @second_source.notify "two-one"
103
+ @second_source.notify "two-two"
104
+
105
+ expect(["one-one", "two-two"]).to become_equal_to { @result }
106
+ end
107
+ end
108
+ end
109
+ end