standard-procedure-plumbing 0.2.1 → 0.3.0

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: 00f89cef3ca3db2daf437446af1788755906434de2ec2d4e6e81130b26190f84
4
- data.tar.gz: 44d27c6f52de7bbc24c488f741035d0a9fe4341036c423e51b5031896ca9f50a
3
+ metadata.gz: 245665ccd98b92e9b49ae532c7efb1edb155b895e6db935cee4de16d8039a96c
4
+ data.tar.gz: 0a2b802e5e09dbc6f08259f0812c03bdb869002d90e04d5186f2ee716081ae44
5
5
  SHA512:
6
- metadata.gz: 6785c35e5596df5718a8ce458ced0713abbe3d18516a90b6353c806da8f867d3bf4d48d70b8b31b15b7c23a491fa2c855c091250aa96bdf97595f813b53e8e18
7
- data.tar.gz: 22169d08af47f789dd5a950635ad46598c6a77f402a2221ed719f369df00b065185b402f14ba2dd2dd82b54cac8774d67d1adc2c07524307a644be11707d7956
6
+ metadata.gz: 84678167d2319c1e6af1a502485351ec3cb5decc4a6313396a89503e0f04a52803062081eec15fafdf30b55b93ce5bf8cbd6109d4a4872feabeb78fd2d9c959a
7
+ data.tar.gz: 6a49385767a68aa48c44d110016312563b6517963fd7b338d3b6b0d2106b759d1dc22bd324bec573b3e2b4b1b57aa4dfbc2df19b6ce71e71ed0a51666e3bc3c0
data/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ ## [0.3.0] - 2024-08-28
2
+
3
+ - Added Plumbing::Valve
4
+ - Reimplemented Plumbing::Pipe to use Plumbing::Valve
5
+
6
+ ## [0.2.2] - 2024-08-25
7
+
8
+ - Added Plumbing::RubberDuck
9
+
1
10
  ## [0.2.1] - 2024-08-25
2
11
 
3
12
  - Split the Pipe implementation between the Pipe and EventDispatcher
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,184 +1,388 @@
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.
10
-
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`.
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.
12
49
 
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
72
+
73
+ private
31
74
 
32
- perform :add_first
33
- perform :add_second
34
- perform :add_third
75
+ def add_first(input) = input << "first"
35
76
 
36
- private
77
+ def add_second(input) = input << "second"
37
78
 
38
- def add_first input
39
- input << "first"
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
53
141
 
54
- BuildSequence.new.call 1
55
- # => Plumbing::PreconditionError("must_be_an_array")
142
+ private
56
143
 
57
- BuildSequence.new.call ["extra element"]
58
- # => Plumbing::PostconditionError("must_have_three_elements")
144
+ def add_first(input) = input << "first"
145
+
146
+ def add_third(input) = input << "third"
147
+ end
148
+
149
+ BuildSequenceWithExternalStep.new.call([])
150
+ # => ["first", "external", "third"]
59
151
  ```
60
152
 
61
- ## Plumbing::Pipe - a composable observer
153
+ ## Plumbing::Valve - safe asynchronous objects
62
154
 
63
- [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)).
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.
64
156
 
65
- [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.
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.
66
158
 
67
- ### Usage
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. Commands have no return value so when the message is sent, the caller does not block and execution continues immediately. Queries return a value so the caller must block until the actor has returned a value.
160
+
161
+ 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.
162
+
163
+ 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.
164
+
165
+ 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.
166
+
167
+ ### Usage
168
+
169
+ [Defining an actor](/spec/examples/valve_spec.rb)
68
170
 
69
- A simple observer:
70
171
  ```ruby
71
- require "plumbing"
172
+ require "plumbing"
173
+
174
+ class Employee
175
+ attr_reader :name, :job_title
176
+
177
+ include Plumbing::Valve
178
+ query :name, :job_title
179
+ command :promote
180
+
181
+ def initialize(name)
182
+ @name = name
183
+ @job_title = "Sales assistant"
184
+ end
72
185
 
73
- @source = Plumbing::Pipe.start
186
+ def promote
187
+ sleep 0.5
188
+ @job_title = "Sales manager"
189
+ end
190
+ end
191
+ ```
74
192
 
75
- @observer = @source.add_observer do |event|
76
- puts event.type
77
- end
193
+ [Acting inline](/spec/examples/valve_spec.rb) with no concurrency
78
194
 
79
- @source.notify "something_happened", message: "But what was it?"
80
- # => "something_happened"
195
+ ```ruby
196
+ require "plumbing"
197
+
198
+ @person = Employee.start "Alice"
199
+
200
+ puts @person.name
201
+ # => "Alice"
202
+ puts @person.job_title
203
+ # => "Sales assistant"
204
+
205
+ @person.promote
206
+ # this will block for 0.5 seconds
207
+ puts @person.job_title
208
+ # => "Sales manager"
81
209
  ```
82
210
 
83
- Simple filtering:
211
+ [Using fibers](/spec/examples/valve_spec.rb) with concurrency but no parallelism
212
+
84
213
  ```ruby
85
- require "plumbing"
214
+ require "plumbing"
215
+ require "async"
86
216
 
87
- @source = Plumbing::Pipe.start
217
+ Plumbing.configure mode: :async
218
+ @person = Employee.start "Alice"
88
219
 
89
- @filter = Plumbing::Filter.start source: @source do |event|
90
- %w[important urgent].include? event.type
91
- end
220
+ puts @person.name
221
+ # => "Alice"
222
+ puts @person.job_title
223
+ # => "Sales assistant"
92
224
 
93
- @observer = @filter.add_observer do |event|
94
- puts event.type
95
- end
225
+ @person.promote
226
+ # this will return immediately without blocking
227
+ puts @person.job_title
228
+ # => "Sales manager" (this will block for 0.5s because #job_title query will not start until the #promote command has completed)
229
+ ```
230
+
231
+ ## Plumbing::Pipe - a composable observer
232
+
233
+ [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)).
234
+
235
+ [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.
96
236
 
97
- @source.notify "important", message: "ALERT! ALERT!"
98
- # => "important"
237
+ 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.
99
238
 
100
- @source.notify "unimportant", message: "Nothing to see here"
101
- # => <no output>
239
+ ### Usage
240
+
241
+ [A simple observer](/spec/examples/pipe_spec.rb):
242
+ ```ruby
243
+ require "plumbing"
244
+
245
+ @source = Plumbing::Pipe.start
246
+ @observer = @source.add_observer do |event|
247
+ puts event.type
248
+ end
249
+
250
+ @source.notify "something_happened", message: "But what was it?"
251
+ # => "something_happened"
102
252
  ```
103
253
 
104
- Custom filtering:
254
+ [Simple filtering](/spec/examples/pipe_spec.rb):
105
255
  ```ruby
106
- require "plumbing"
256
+ require "plumbing"
107
257
 
108
- class EveryThirdEvent < Plumbing::CustomFilter
109
- def initialize source:
110
- super source: source
111
- @events = []
258
+ @source = Plumbing::Pipe.start
259
+ @filter = Plumbing::Filter.start source: @source do |event|
260
+ %w[important urgent].include? event.type
261
+ end
262
+ @observer = @filter.add_observer do |event|
263
+ puts event.type
112
264
  end
113
265
 
114
- def received event
115
- @events << event
116
- # if we've already stored 2 events in the buffer then broadcast the newest event and clear the buffer
117
- if @events.count >= 2
118
- @events.clear
119
- self << event
266
+ @source.notify "important", message: "ALERT! ALERT!"
267
+ # => "important"
268
+ @source.notify "unimportant", message: "Nothing to see here"
269
+ # => <no output>
270
+ ```
271
+
272
+ [Custom filtering](/spec/examples/pipe_spec.rb):
273
+ ```ruby
274
+ require "plumbing"
275
+ class EveryThirdEvent < Plumbing::CustomFilter
276
+ def initialize source:
277
+ super source: source
278
+ @events = []
120
279
  end
121
- end
122
- end
123
280
 
124
- @source = Plumbing::Pipe.start
125
- @filter = EveryThirdEvent.new(source: @source)
281
+ def received event
282
+ # store this event into our buffer
283
+ @events << event
284
+ # if this is the third event we've received then clear the buffer and broadcast the latest event
285
+ if @events.count >= 3
286
+ @events.clear
287
+ self << event
288
+ end
289
+ end
290
+ end
126
291
 
127
- @observer = @filter.add_observer do |event|
128
- puts event.type
129
- end
292
+ @source = Plumbing::Pipe.start
293
+ @filter = EveryThirdEvent.start(source: @source)
294
+ @observer = @filter.add_observer do |event|
295
+ puts event.type
296
+ end
130
297
 
131
- 1.upto 10 do |i|
132
- @source.notify i.to_s
133
- end
134
- # => "3"
135
- # => "6"
136
- # => "9"
298
+ 1.upto 10 do |i|
299
+ @source.notify i.to_s
300
+ end
301
+ # => "3"
302
+ # => "6"
303
+ # => "9"
137
304
  ```
138
305
 
139
- Joining multiple sources
306
+ [Joining multiple sources](/spec/examples/pipe_spec.rb):
140
307
  ```ruby
141
- require "plumbing"
308
+ require "plumbing"
142
309
 
143
- @first_source = Plumbing::Pipe.start
144
- @second_source = Plumbing::Pipe.start
310
+ @first_source = Plumbing::Pipe.start
311
+ @second_source = Plumbing::Pipe.start
145
312
 
146
- @junction = Plumbing::Junction.start @first_source, @second_source
313
+ @junction = Plumbing::Junction.start @first_source, @second_source
147
314
 
148
- @observer = @junction.add_observer do |event|
149
- puts event.type
150
- end
315
+ @observer = @junction.add_observer do |event|
316
+ puts event.type
317
+ end
151
318
 
152
- @first_source.notify "one"
153
- # => "one"
154
- @second_source.notify "two"
155
- # => "two"
319
+ @first_source.notify "one"
320
+ # => "one"
321
+ @second_source.notify "two"
322
+ # => "two"
156
323
  ```
157
324
 
158
- Dispatching events asynchronously (using Fibers)
325
+ [Dispatching events asynchronously (using Fibers)](/spec/examples/pipe_spec.rb):
159
326
  ```ruby
160
- require "plumbing"
161
- require "plumbing/event_dispatcher/fiber"
162
- require "async"
327
+ require "plumbing"
328
+ require "async"
163
329
 
164
- # `limit` controls how many fibers can dispatch events concurrently - the default is 4
165
- @first_source = Plumbing::Pipe.start dispatcher: Plumbing::EventDispatcher::Fiber.new limit: 8
166
- @second_source = Plumbing::Pipe.start dispatcher: Plumbing::EventDispatcher::Fiber.new limit: 2
330
+ Plumbing.configure mode: :async
167
331
 
168
- @junction = Plumbing::Junction.start @first_source, @second_source, dispatcher: Plumbing::EventDispatcher::Fiber.new
332
+ Sync do
333
+ @first_source = Plumbing::Pipe.start
334
+ @second_source = Plumbing::Pipe.start
169
335
 
170
- @filter = Plumbing::Filter.start source: @junction, dispatcher: Plumbing::EventDispatcher::Fibernew do |event|
171
- %w[one-one two-two].include? event.type
172
- end
336
+ @junction = Plumbing::Junction.start @first_source, @second_source
173
337
 
174
- Sync do
175
- @first_source.notify "one-one"
176
- @first_source.notify "one-two"
177
- @second_source.notify "two-one"
178
- @second_source.notify "two-two"
179
- end
338
+ @filter = Plumbing::Filter.start source: @junction do |event|
339
+ %w[one-one two-two].include? event.type
340
+ end
341
+
342
+ @first_source.notify "one-one"
343
+ @first_source.notify "one-two"
344
+ @second_source.notify "two-one"
345
+ @second_source.notify "two-two"
346
+ end
180
347
  ```
181
348
 
349
+ ## Plumbing::RubberDuck - duck types and type-casts
350
+
351
+ 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).
352
+
353
+
354
+ ### Usage
355
+
356
+ Define your interface (Person in this example), then cast your objects (instances of PersonData and CarData).
357
+
358
+ [Casting objects as duck-types](/spec/examples/rubber_duck_spec.rb):
359
+ ```ruby
360
+ require "plumbing"
361
+
362
+ Person = Plumbing::RubberDuck.define :first_name, :last_name, :email
363
+ LikesFood = Plumbing::RubberDuck.define :favourite_food
364
+
365
+ PersonData = Struct.new(:first_name, :last_name, :email, :favourite_food)
366
+ CarData = Struct.new(:make, :model, :colour)
367
+
368
+ @porsche_911 = CarData.new "Porsche", "911", "black"
369
+ @person = @porsche_911.as Person
370
+ # => Raises a TypeError as CarData does not respond_to #first_name, #last_name, #email
371
+
372
+ @alice = PersonData.new "Alice", "Aardvark", "alice@example.com", "Ice cream"
373
+ @person = @alice.as Person
374
+ @person.first_name
375
+ # => "Alice"
376
+ @person.email
377
+ # => "alice@example.com"
378
+ @person.favourite_food
379
+ # => NoMethodError - #favourite_food is not part of the Person rubber duck (even though it is part of the underlying PersonData struct)
380
+
381
+ # Cast our Person into a LikesFood rubber duck
382
+ @hungry = @person.as LikesFood
383
+ @hungry.favourite_food
384
+ # => "Ice cream"
385
+ ```
182
386
 
183
387
  ## Installation
184
388
 
@@ -194,7 +398,6 @@ Then:
194
398
  require 'plumbing'
195
399
  ```
196
400
 
197
-
198
401
  ## Development
199
402
 
200
403
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -0,0 +1 @@
1
+ 2309f738a6739650f259c456af69796e59c1de214579a8a274d56b7ccc8176e9e59cb0e63d744bd5a6ed40e93f4c57a368c3de2bc525163e90da9467a58c6387
@@ -0,0 +1 @@
1
+ f9f003afc58c61f5cf75127e1c92e95aebeb8f414e47bbd80661cedfbe379709ba22246f759e6ba1c8a8986f57ef6cb5d615157633e18abc6a3fa518325dc544