standard-procedure-plumbing 0.2.2 → 0.3.1

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: 6074870313ece34eb4b9565602db4de70bb5b7e14e8a9051d85bb46b6fc64bf5
4
- data.tar.gz: a81292b0ad9e87dfcce61d531e891c1b4ed25ccda3331ceb731c082c6e9c7c16
3
+ metadata.gz: 9a111ce599942b5e33b6928cede476b75e56fd6574625bf50ca82d6d75f3e718
4
+ data.tar.gz: b1b4c48062aec9d77ec3b917c25723fae8ff2cc4075d9f91c699da3f165b7c84
5
5
  SHA512:
6
- metadata.gz: 45827e921f7bc272e0688a6477d405a81e8c9ea64405078782fbf5aa45658066bf7f9c5503692f4f6eadfdbb08d21221eb4e0b0731654e4808e0f7ec1111f9bb
7
- data.tar.gz: 1ba319545accf7051393845b1883d5eb69aeacba182c86f26ca6e95e7761711e922904d2ec5427872e8dcd6ccdf5fba25737fe5c20a887cc06ba23ab2184c81a
6
+ metadata.gz: 6d2de706d57ef380e67fcd9d79b3d202ca0741f7ed24552ad62bc63944f1c0a82cdd6123560a6d79a85099218db5c9c966ee602a0ef281ea7d5b0305e1f5a707
7
+ data.tar.gz: 47de86307b817c0d0399bc06bcac5f78eea3629b93725b785f1dc6a442a300a03ceadde6627f2d198ea296524f584f29a90fa3ec52e5e635507c024241fec5da
data/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ ## [0.3.1] - 2024-09-03
2
+
3
+ - Added `ignore_result` for queries on Plumbing::Valves
4
+
5
+ ## [0.3.0] - 2024-08-28
6
+
7
+ - Added Plumbing::Valve
8
+ - Reimplemented Plumbing::Pipe to use Plumbing::Valve
9
+
1
10
  ## [0.2.2] - 2024-08-25
2
11
 
3
12
  - Added Plumbing::RubberDuck
data/CODE_OF_CONDUCT.md CHANGED
@@ -1,84 +1,5 @@
1
1
  # Contributor Covenant Code of Conduct
2
2
 
3
- ## Our Pledge
3
+ **BE NICE**
4
4
 
5
- We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
6
-
7
- We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
8
-
9
- ## Our Standards
10
-
11
- Examples of behavior that contributes to a positive environment for our community include:
12
-
13
- * Demonstrating empathy and kindness toward other people
14
- * Being respectful of differing opinions, viewpoints, and experiences
15
- * Giving and gracefully accepting constructive feedback
16
- * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
17
- * Focusing on what is best not just for us as individuals, but for the overall community
18
-
19
- Examples of unacceptable behavior include:
20
-
21
- * The use of sexualized language or imagery, and sexual attention or
22
- advances of any kind
23
- * Trolling, insulting or derogatory comments, and personal or political attacks
24
- * Public or private harassment
25
- * Publishing others' private information, such as a physical or email
26
- address, without their explicit permission
27
- * Other conduct which could reasonably be considered inappropriate in a
28
- professional setting
29
-
30
- ## Enforcement Responsibilities
31
-
32
- Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
33
-
34
- Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
35
-
36
- ## Scope
37
-
38
- This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
39
-
40
- ## Enforcement
41
-
42
- Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at rahoulb@echodek.co. All complaints will be reviewed and investigated promptly and fairly.
43
-
44
- All community leaders are obligated to respect the privacy and security of the reporter of any incident.
45
-
46
- ## Enforcement Guidelines
47
-
48
- Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
49
-
50
- ### 1. Correction
51
-
52
- **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
53
-
54
- **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
55
-
56
- ### 2. Warning
57
-
58
- **Community Impact**: A violation through a single incident or series of actions.
59
-
60
- **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
61
-
62
- ### 3. Temporary Ban
63
-
64
- **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
65
-
66
- **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
67
-
68
- ### 4. Permanent Ban
69
-
70
- **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
71
-
72
- **Consequence**: A permanent ban from any sort of public interaction within the community.
73
-
74
- ## Attribution
75
-
76
- This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
77
- available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
78
-
79
- Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
80
-
81
- [homepage]: https://www.contributor-covenant.org
82
-
83
- For answers to common questions about this code of conduct, see the FAQ at
84
- https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
5
+ If people think you're being a dick, you're probably being a dick, so stop it.
data/README.md CHANGED
@@ -1,215 +1,409 @@
1
1
  # Plumbing
2
2
 
3
+ ## Configuration
4
+
5
+ The most important configuration setting is the `mode`, which governs how messages are handled by Valves.
6
+
7
+ By default it is `:inline`, so every command or query is handled synchronously.
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)).
10
+
11
+ The `timeout` setting is used when performing queries - it defaults to 30s.
12
+
13
+ ```ruby
14
+ require "plumbing"
15
+ puts Plumbing.config.mode
16
+ # => :inline
17
+
18
+ Plumbing.configure mode: :async, timeout: 10
19
+
20
+ puts Plumbing.config.mode
21
+ # => :async
22
+ ```
23
+
24
+ If you are running a test suite, you can temporarily update the configuration by passing a block.
25
+
26
+ ```ruby
27
+ require "plumbing"
28
+ puts Plumbing.config.mode
29
+ # => :inline
30
+
31
+ Plumbing.configure mode: :async do
32
+ puts Plumbing.config.mode
33
+ # => :async
34
+ first_test
35
+ second_test
36
+ end
37
+
38
+ puts Plumbing.config.mode
39
+ # => :inline
40
+ ```
41
+
3
42
  ## Plumbing::Pipeline - transform data through a pipeline
4
43
 
5
- Define a sequence of operations that proceed in order, passing their output from one operation as the input to another.
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.
6
45
 
7
46
  Use `perform` to define a step that takes some input and returns a different output.
8
- Use `execute` to define a step that takes some input and returns that same input.
9
- Use `embed` to define a step that uses another `Plumbing::Chain` class to generate the 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.
10
49
 
11
- 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`.
12
-
13
- If you don't want to use dry-validation, you can instead define a `pre_condition` (although there's nothing to stop you defining a contract as well as pre_conditions - with the contract being verified first).
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.
14
51
 
15
52
  You can also verify that the output generated is as expected by defining a `post_condition`.
16
53
 
17
54
  ### Usage:
18
55
 
56
+ [Building an array using multiple steps with a pre-condition and post-condition](/spec/examples/pipeline_spec.rb)
57
+
19
58
  ```ruby
20
- require "plumbing"
21
- class BuildSequence < Plumbing::Pipeline
22
- pre_condition :must_be_an_array do |input|
23
- # you could replace this with a `validate` definition (using a Dry::Validation::Contract) if you prefer
24
- input.is_a? Array
25
- end
59
+ require "plumbing"
60
+ class BuildArray < Plumbing::Pipeline
61
+ perform :add_first
62
+ perform :add_second
63
+ perform :add_third
64
+
65
+ pre_condition :must_be_an_array do |input|
66
+ input.is_a? Array
67
+ end
26
68
 
27
- post_condition :must_have_three_elements do |output|
28
- # this is a stupid post-condition but 🤷🏾‍♂️, this is just an example
29
- output.length == 3
30
- end
69
+ post_condition :must_have_three_elements do |output|
70
+ output.length == 3
71
+ end
31
72
 
32
- perform :add_first
33
- perform :add_second
34
- perform :add_third
73
+ private
35
74
 
36
- private
75
+ def add_first(input) = input << "first"
37
76
 
38
- def add_first input
39
- input << "first"
77
+ def add_second(input) = input << "second"
78
+
79
+ def add_third(input) = input << "third"
40
80
  end
41
81
 
42
- def add_second input
43
- input << "second"
82
+ BuildArray.new.call []
83
+ # => ["first", "second", "third"]
84
+
85
+ BuildArray.new.call 1
86
+ # => Plumbing::PreconditionError("must_be_an_array")
87
+
88
+ BuildArray.new.call ["extra element"]
89
+ # => Plumbing::PostconditionError("must_have_three_elements")
90
+ ```
91
+
92
+ [Validating input parameters with a contract](/spec/examples/pipeline_spec.rb)
93
+ ```ruby
94
+ require "plumbing"
95
+ require "dry/validation"
96
+
97
+ class SayHello < Plumbing::Pipeline
98
+ validate_with "SayHello::Input"
99
+ perform :say_hello
100
+
101
+ private
102
+
103
+ def say_hello input
104
+ "Hello #{input[:name]} - I will now send a load of annoying marketing messages to #{input[:email]}"
105
+ end
106
+
107
+ class Input < Dry::Validation::Contract
108
+ params do
109
+ required(:name).filled(:string)
110
+ required(:email).filled(:string)
111
+ end
112
+ rule :email do
113
+ key.failure("must be a valid email") unless /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i.match? value
114
+ end
115
+ end
44
116
  end
45
117
 
46
- def add_third input
47
- input << "third"
118
+ 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
120
+
121
+ SayHello.new.call(some: "other data")
122
+ # => Plumbing::PreConditionError
123
+ ```
124
+
125
+ [Building a pipeline through composition](/spec/examples/pipeline_spec.rb)
126
+
127
+ ```ruby
128
+ require "plumbing"
129
+ class ExternalStep < Plumbing::Pipeline
130
+ perform :add_item_to_array
131
+
132
+ private
133
+
134
+ def add_item_to_array(input) = input << "external"
48
135
  end
49
- end
50
136
 
51
- BuildSequence.new.call []
52
- # => ["first", "second", "third"]
137
+ class BuildSequenceWithExternalStep < Plumbing::Pipeline
138
+ perform :add_first
139
+ perform :add_second, using: "ExternalStep"
140
+ perform :add_third
141
+
142
+ private
143
+
144
+ def add_first(input) = input << "first"
53
145
 
54
- BuildSequence.new.call 1
55
- # => Plumbing::PreconditionError("must_be_an_array")
146
+ def add_third(input) = input << "third"
147
+ end
56
148
 
57
- BuildSequence.new.call ["extra element"]
58
- # => Plumbing::PostconditionError("must_have_three_elements")
149
+ BuildSequenceWithExternalStep.new.call([])
150
+ # => ["first", "external", "third"]
59
151
  ```
60
152
 
153
+ ## Plumbing::Valve - safe asynchronous objects
61
154
 
62
- ## Plumbing::Pipe - a composable observer
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.
63
156
 
64
- [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)).
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.
65
158
 
66
- [Plumbing::Pipe](lib/plumbing/pipe.rb) makes observers "composable". Instead of simply registering for notifications from the observable, we observe a stream of notifications, which could be produced by multiple observables, all being sent through the same pipe. We can then chain observers together, composing a "pipeline" of operations from a single source of events.
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.
67
160
 
68
- ### Usage
161
+ - 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
+ - Queries return a value so the caller blocks until the actor has returned a value
163
+ - 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
+ - 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
+
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.
167
+
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.
169
+
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.
171
+
172
+ ### Usage
173
+
174
+ [Defining an actor](/spec/examples/valve_spec.rb)
175
+
176
+ ```ruby
177
+ require "plumbing"
178
+
179
+ class Employee
180
+ attr_reader :name, :job_title
181
+
182
+ include Plumbing::Valve
183
+ query :name, :job_title, :greet_slowly
184
+ command :promote
185
+
186
+ def initialize(name)
187
+ @name = name
188
+ @job_title = "Sales assistant"
189
+ end
190
+
191
+ def promote
192
+ sleep 0.5
193
+ @job_title = "Sales manager"
194
+ end
195
+
196
+ def greet_slowly
197
+ sleep 0.2
198
+ "H E L L O"
199
+ end
200
+ end
201
+ ```
202
+
203
+ [Acting inline](/spec/examples/valve_spec.rb) with no concurrency
69
204
 
70
- A simple observer:
71
205
  ```ruby
72
- require "plumbing"
206
+ require "plumbing"
207
+
208
+ @person = Employee.start "Alice"
209
+
210
+ puts @person.name
211
+ # => "Alice"
212
+ puts @person.job_title
213
+ # => "Sales assistant"
73
214
 
74
- @source = Plumbing::Pipe.start
215
+ @person.promote
216
+ # this will block for 0.5 seconds
217
+ puts @person.job_title
218
+ # => "Sales manager"
75
219
 
76
- @observer = @source.add_observer do |event|
77
- puts event.type
78
- end
220
+ @person.greet_slowly
221
+ # this will block for 0.2 seconds before returning "H E L L O"
79
222
 
80
- @source.notify "something_happened", message: "But what was it?"
81
- # => "something_happened"
223
+ @person.greet_slowly(ignore_result: true)
224
+ # this will block for 0.2 seconds (as the mode is :inline) before returning nil
82
225
  ```
83
226
 
84
- Simple filtering:
227
+ [Using fibers](/spec/examples/valve_spec.rb) with concurrency but no parallelism
228
+
85
229
  ```ruby
86
- require "plumbing"
230
+ require "plumbing"
231
+ require "async"
232
+
233
+ Plumbing.configure mode: :async
234
+ @person = Employee.start "Alice"
235
+
236
+ puts @person.name
237
+ # => "Alice"
238
+ puts @person.job_title
239
+ # => "Sales assistant"
240
+
241
+ @person.promote
242
+ # this will return immediately without blocking
243
+ puts @person.job_title
244
+ # => "Sales manager" (this will block for 0.5s because #job_title query will not start until the #promote command has completed)
245
+
246
+ @person.greet_slowly
247
+ # this will block for 0.2 seconds before returning "H E L L O"
248
+
249
+ @person.greet_slowly(ignore_result: true)
250
+ # this will not block and returns nil
251
+ ```
252
+
253
+ ## Plumbing::Pipe - a composable observer
87
254
 
88
- @source = Plumbing::Pipe.start
255
+ [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
+
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.
258
+
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.
89
260
 
90
- @filter = Plumbing::Filter.start source: @source do |event|
91
- %w[important urgent].include? event.type
92
- end
261
+ ### Usage
93
262
 
94
- @observer = @filter.add_observer do |event|
95
- puts event.type
96
- end
263
+ [A simple observer](/spec/examples/pipe_spec.rb):
264
+ ```ruby
265
+ require "plumbing"
97
266
 
98
- @source.notify "important", message: "ALERT! ALERT!"
99
- # => "important"
267
+ @source = Plumbing::Pipe.start
268
+ @observer = @source.add_observer do |event|
269
+ puts event.type
270
+ end
100
271
 
101
- @source.notify "unimportant", message: "Nothing to see here"
102
- # => <no output>
272
+ @source.notify "something_happened", message: "But what was it?"
273
+ # => "something_happened"
103
274
  ```
104
275
 
105
- Custom filtering:
276
+ [Simple filtering](/spec/examples/pipe_spec.rb):
106
277
  ```ruby
107
- require "plumbing"
278
+ require "plumbing"
108
279
 
109
- class EveryThirdEvent < Plumbing::CustomFilter
110
- def initialize source:
111
- super source: source
112
- @events = []
280
+ @source = Plumbing::Pipe.start
281
+ @filter = Plumbing::Filter.start source: @source do |event|
282
+ %w[important urgent].include? event.type
113
283
  end
284
+ @observer = @filter.add_observer do |event|
285
+ puts event.type
286
+ end
287
+
288
+ @source.notify "important", message: "ALERT! ALERT!"
289
+ # => "important"
290
+ @source.notify "unimportant", message: "Nothing to see here"
291
+ # => <no output>
292
+ ```
114
293
 
115
- def received event
116
- @events << event
117
- # if we've already stored 2 events in the buffer then broadcast the newest event and clear the buffer
118
- if @events.count >= 2
119
- @events.clear
120
- self << event
294
+ [Custom filtering](/spec/examples/pipe_spec.rb):
295
+ ```ruby
296
+ require "plumbing"
297
+ class EveryThirdEvent < Plumbing::CustomFilter
298
+ def initialize source:
299
+ super source: source
300
+ @events = []
121
301
  end
122
- end
123
- end
124
302
 
125
- @source = Plumbing::Pipe.start
126
- @filter = EveryThirdEvent.new(source: @source)
303
+ def received event
304
+ # store this event into our buffer
305
+ @events << event
306
+ # if this is the third event we've received then clear the buffer and broadcast the latest event
307
+ if @events.count >= 3
308
+ @events.clear
309
+ self << event
310
+ end
311
+ end
312
+ end
127
313
 
128
- @observer = @filter.add_observer do |event|
129
- puts event.type
130
- end
314
+ @source = Plumbing::Pipe.start
315
+ @filter = EveryThirdEvent.start(source: @source)
316
+ @observer = @filter.add_observer do |event|
317
+ puts event.type
318
+ end
131
319
 
132
- 1.upto 10 do |i|
133
- @source.notify i.to_s
134
- end
135
- # => "3"
136
- # => "6"
137
- # => "9"
320
+ 1.upto 10 do |i|
321
+ @source.notify i.to_s
322
+ end
323
+ # => "3"
324
+ # => "6"
325
+ # => "9"
138
326
  ```
139
327
 
140
- Joining multiple sources
328
+ [Joining multiple sources](/spec/examples/pipe_spec.rb):
141
329
  ```ruby
142
- require "plumbing"
330
+ require "plumbing"
143
331
 
144
- @first_source = Plumbing::Pipe.start
145
- @second_source = Plumbing::Pipe.start
332
+ @first_source = Plumbing::Pipe.start
333
+ @second_source = Plumbing::Pipe.start
146
334
 
147
- @junction = Plumbing::Junction.start @first_source, @second_source
335
+ @junction = Plumbing::Junction.start @first_source, @second_source
148
336
 
149
- @observer = @junction.add_observer do |event|
150
- puts event.type
151
- end
337
+ @observer = @junction.add_observer do |event|
338
+ puts event.type
339
+ end
152
340
 
153
- @first_source.notify "one"
154
- # => "one"
155
- @second_source.notify "two"
156
- # => "two"
341
+ @first_source.notify "one"
342
+ # => "one"
343
+ @second_source.notify "two"
344
+ # => "two"
157
345
  ```
158
346
 
159
- Dispatching events asynchronously (using Fibers)
347
+ [Dispatching events asynchronously (using Fibers)](/spec/examples/pipe_spec.rb):
160
348
  ```ruby
161
- require "plumbing"
162
- require "plumbing/event_dispatcher/fiber"
163
- require "async"
349
+ require "plumbing"
350
+ require "async"
164
351
 
165
- # `limit` controls how many fibers can dispatch events concurrently - the default is 4
166
- @first_source = Plumbing::Pipe.start dispatcher: Plumbing::EventDispatcher::Fiber.new limit: 8
167
- @second_source = Plumbing::Pipe.start dispatcher: Plumbing::EventDispatcher::Fiber.new limit: 2
352
+ Plumbing.configure mode: :async
168
353
 
169
- @junction = Plumbing::Junction.start @first_source, @second_source, dispatcher: Plumbing::EventDispatcher::Fiber.new
354
+ Sync do
355
+ @first_source = Plumbing::Pipe.start
356
+ @second_source = Plumbing::Pipe.start
170
357
 
171
- @filter = Plumbing::Filter.start source: @junction, dispatcher: Plumbing::EventDispatcher::Fibernew do |event|
172
- %w[one-one two-two].include? event.type
173
- end
358
+ @junction = Plumbing::Junction.start @first_source, @second_source
174
359
 
175
- Sync do
176
- @first_source.notify "one-one"
177
- @first_source.notify "one-two"
178
- @second_source.notify "two-one"
179
- @second_source.notify "two-two"
180
- end
181
- ```
360
+ @filter = Plumbing::Filter.start source: @junction do |event|
361
+ %w[one-one two-two].include? event.type
362
+ end
182
363
 
364
+ @first_source.notify "one-one"
365
+ @first_source.notify "one-two"
366
+ @second_source.notify "two-one"
367
+ @second_source.notify "two-two"
368
+ end
369
+ ```
183
370
 
184
371
  ## Plumbing::RubberDuck - duck types and type-casts
185
372
 
186
- 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 limits you to sending those messages and no others.
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).
187
374
 
188
375
 
189
376
  ### Usage
190
377
 
191
378
  Define your interface (Person in this example), then cast your objects (instances of PersonData and CarData).
192
379
 
380
+ [Casting objects as duck-types](/spec/examples/rubber_duck_spec.rb):
193
381
  ```ruby
194
- require "plumbing"
195
-
196
- Person = Plumbing::RubberDuck.define :first_name, :last_name, :email
197
-
198
- PersonData = Struct.new(:first_name, :last_name, :email, :favourite_food)
199
- CarData = Struct.new(:make, :model, :colour)
200
-
201
- @porsche_911 = CarData.new "Porsche", "911", "black"
202
- @person = @porsche_911.as Person
203
- # => Raises a TypeError
204
-
205
- @alice = PersonData.new "Alice", "Aardvark", "alice@example.com", "Ice cream"
206
- @person = @alice.as Person
207
- @person.first_name
208
- # => "Alice"
209
- @person.email
210
- # => "alice@example.com"
211
- @person.favourite_food
212
- # => NoMethodError - even though :favourite_food is a field in PersonData, it is not included in the definition of Person so cannot be accessed through the RubberDuck type
382
+ require "plumbing"
383
+
384
+ Person = Plumbing::RubberDuck.define :first_name, :last_name, :email
385
+ LikesFood = Plumbing::RubberDuck.define :favourite_food
386
+
387
+ PersonData = Struct.new(:first_name, :last_name, :email, :favourite_food)
388
+ CarData = Struct.new(:make, :model, :colour)
389
+
390
+ @porsche_911 = CarData.new "Porsche", "911", "black"
391
+ @person = @porsche_911.as Person
392
+ # => Raises a TypeError as CarData does not respond_to #first_name, #last_name, #email
393
+
394
+ @alice = PersonData.new "Alice", "Aardvark", "alice@example.com", "Ice cream"
395
+ @person = @alice.as Person
396
+ @person.first_name
397
+ # => "Alice"
398
+ @person.email
399
+ # => "alice@example.com"
400
+ @person.favourite_food
401
+ # => NoMethodError - #favourite_food is not part of the Person rubber duck (even though it is part of the underlying PersonData struct)
402
+
403
+ # Cast our Person into a LikesFood rubber duck
404
+ @hungry = @person.as LikesFood
405
+ @hungry.favourite_food
406
+ # => "Ice cream"
213
407
  ```
214
408
 
215
409
  ## Installation
@@ -0,0 +1 @@
1
+ f9f003afc58c61f5cf75127e1c92e95aebeb8f414e47bbd80661cedfbe379709ba22246f759e6ba1c8a8986f57ef6cb5d615157633e18abc6a3fa518325dc544