pipeable 0.4.0 → 0.5.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: 3e0aa08638b53d5dc7005d8754db2e991a9529d439ebedef96baa7fb4ef0e67b
4
- data.tar.gz: 16cab1671faa230b629f19c68d2b16835e3b2e9406085b8e08ae1281194c2273
3
+ metadata.gz: 2467c8bb8dda8efd0a2e4fc5a78765f8abaeab65c288c380d9c7358244ba5aaf
4
+ data.tar.gz: 96c96e5e12d25f81dd9c7e83b59850691036513b8456acab4868b93a71083f22
5
5
  SHA512:
6
- metadata.gz: b4a029e0fe184140e2ccb0649753d19c2250e5231df82ef2fd8961e74e622330090891288db8654e5d0282ff41b459587901b14dba37426407d6fd31ba6b53cc
7
- data.tar.gz: 31a1d08c5c4f068fdf04bfcc92b21308ff1eee4e551da94db16557d7b6c2b3621c90728519addf1ac0e101134b2056450017f1e0f7cab83ee098dcc0cb2e5174
6
+ metadata.gz: 94f1002860d093423864900306a266a8c5d452f186f40e6053c326fc9a689cf0b7edded0a60f92d00dd1de14f906315f6b88a7987fb0b67511f9a92fdd73f160
7
+ data.tar.gz: 767a44496263b160871c38a7bef274eacc089dbf641fdb60a7879f07e584603c0fc125cf9b285c156a75daff4e6cc856c5b027307f87a8f1f34c0876dcba33ce
checksums.yaml.gz.sig CHANGED
Binary file
data/README.adoc CHANGED
@@ -10,11 +10,12 @@
10
10
  :dry_validation_link: link:https://dry-rb.org/gems/dry-validation[Dry Validation]
11
11
  :function_composition_link: link:https://alchemists.io/articles/ruby_function_composition[Function Composition]
12
12
  :infusible_link: link:https://alchemists.io/projects/infusible[Infusible]
13
+ :method_parameters_and_arguments_link: link:https://alchemists.io/articles/ruby_method_parameters_and_arguments[Method Parameters And Arguments]
13
14
  :railway_pattern_link: link:https://fsharpforfunandprofit.com/posts/recipe-part2[Railway Pattern]
14
15
 
15
16
  = Pipeable
16
17
 
17
- A DSL for workflows built atop native {function_composition_link} which leverages the {railway_pattern_link}. This allows you to write a sequence of _steps_ that cleanly read from left-to-right or top-to-bottom which results in a success or a failure without having to rely on exceptions which are expensive and should not be used for control flow.
18
+ A DSL for workflows built atop native {function_composition_link} which leverages the {railway_pattern_link}. This allows you to write a sequence of _steps_ that cleanly read from top-to-bottom or left-to-right resulting in a single success or a failure. This allows you to avoid relying on exceptions for expensive control flows and/or complex conditional logic in general.
18
19
 
19
20
  toc::[]
20
21
 
@@ -29,7 +30,7 @@ toc::[]
29
30
  == Requirements
30
31
 
31
32
  . link:https://www.ruby-lang.org[Ruby].
32
- . A strong understanding of {function_composition_link}.
33
+ . A strong understanding of {function_composition_link} and {method_parameters_and_arguments_link}.
33
34
 
34
35
  == Setup
35
36
 
@@ -65,7 +66,7 @@ require "pipeable"
65
66
 
66
67
  == Usage
67
68
 
68
- You can turn any object into a _transaction_ by requiring and including this gem as follows:
69
+ You can turn any object into a _pipe_ by requiring and including this gem as follows:
69
70
 
70
71
  [source,ruby]
71
72
  ----
@@ -100,7 +101,7 @@ class Demo
100
101
  end
101
102
  ----
102
103
 
103
- The above allows `Demo#call` to be a sequence steps which may pass or fail due to all step being {dry_monads_link}. This is the essence of the {railway_pattern_link}.
104
+ The above allows `Demo#call` to be a sequence of steps which may pass or fail due to all steps using {dry_monads_link} for input and output. This is the essence of the {railway_pattern_link}.
104
105
 
105
106
  To execute the above example, you'd only need to pass CSV content to it:
106
107
 
@@ -151,19 +152,44 @@ Then the above would look like this using native Ruby:
151
152
  ).call Success(csv)
152
153
  ----
153
154
 
154
- The problem with native function composition is that it reads backwards by passing your input at the end of all sequential steps. With the `#pipe` method, you have the benefit of allowing your eye to read the code from top to bottom in addition to not having to type multiple _forward composition_ operators.
155
+ The problem with native function composition is that it reads backwards by passing input at the end of all sequential steps. With the `#pipe` method, you have the benefit of allowing your eyes to read from top to bottom while not having to type multiple _forward composition_ operators.
155
156
 
156
157
  === Steps
157
158
 
158
- There are several ways to compose steps for your pipe. As long as all steps succeed, you'll get a successful response. Otherwise, the first step to fail will pass the failure down by skipping all subsequent steps (unless you dynamically attempt to turn the failure into a success). The following sections detail how to mix and match steps for building a robust implementation.
159
+ There are several ways to compose steps for your pipe. As long as all steps succeed, you'll get a successful response. Otherwise, the first step to fail will pass the failure down by skipping all subsequent steps (unless you dynamically turn the failure into a success).
160
+
161
+ Each step expects a {dry_monads_link} `Result` object as input and answers either the same or new `Result` object for consumption by the next step in the pipe. Additionally, each step will either unwrap the `Result` or pass the `Result` through depending on the step's implementation. These details are noted in each step's documentation below complete with _i/o_ (input/output) and _example_ sections.
159
162
 
160
163
  ==== Basic
161
164
 
162
- The following are the basic (default) steps for building for more advanced functionality.
165
+ The following are the basic (default) steps for building custom pipes for which you can mix and match within your own implementation.
166
+
167
+ ===== Alt
168
+
169
+ Allows you to operate on a failure and produce either a success or another failure. This is a convenience wrapper to native {dry_monads_link} `#or` functionality.
170
+
171
+ *I/O*
172
+
173
+ Processes a failure only while expecting you to answer a success or failure.
174
+
175
+ *Example*
176
+
177
+ [source,ruby]
178
+ ----
179
+ pipe %i[a b c], alt { |object| Success object.join("-") } # Success [:a, :b, :c]
180
+ pipe Failure("Danger!"), alt { Success "Resolved" } # Success "Resolved"
181
+ pipe Failure("Danger!"), alt { |object| Failure "Big #{object}" } # Failure "Big Danger!"
182
+ ----
163
183
 
164
184
  ===== As
165
185
 
166
- Allows you to message the input as different output. Example:
186
+ Allows you to message an object as a different result. The first argument is the method but additional positional and/or keyword arguments can be passed along if the method accepts them.
187
+
188
+ *I/O*
189
+
190
+ Processes and answers a success only.
191
+
192
+ *Example*
167
193
 
168
194
  [source,ruby]
169
195
  ----
@@ -174,18 +200,30 @@ pipe Failure("Danger!"), as(:inspect) # Failure "Danger!"
174
200
 
175
201
  ===== Bind
176
202
 
177
- Allows you to perform operations on a successful result only. You are then responsible for answering a success or failure accordingly. This is a convenience wrapper to native {dry_monads_link} `#bind` functionality. Example:
203
+ Allows you to perform operations upon success only. You are then responsible for answering a success or failure accordingly. This is a convenience wrapper to native {dry_monads_link} `#bind` functionality.
204
+
205
+ *I/O*
206
+
207
+ Processes a success only while expecting you to answer a success or failure in return.
208
+
209
+ *Example*
178
210
 
179
211
  [source,ruby]
180
212
  ----
181
- pipe %i[a b c], bind { |input| Success input.join("-") } # Success "a-b-c"
182
- pipe %i[a b c], bind { |input| Failure input } # Failure [:a, :b, :c]
183
- pipe Failure("Danger!"), bind { |input| Success input.join("-") } # Failure "Danger!"
213
+ pipe %i[a b c], bind { |object| Success object.join("-") } # Success "a-b-c"
214
+ pipe %i[a b c], bind { |object| Failure object } # Failure [:a, :b, :c]
215
+ pipe Failure("Danger!"), bind { |object| Success object.join("-") } # Failure "Danger!"
184
216
  ----
185
217
 
186
218
  ===== Check
187
219
 
188
- Allows you to check if the input and messaged object evaluate to `true` or `Success`. When successful, input is passed through as a `Success`. When false, input is passed through as a `Failure`. Example:
220
+ Allows you to check if an object matches the proof (with message). The first argument is your proof while the second argument is the message to send to your proof. A check only passes if the messaged object evaluates to `true` or `Success`. When successful, the object is passed through as a `Success`. When false, the object is passed through as a `Failure`.
221
+
222
+ *I/O*
223
+
224
+ Processes a success only while answering a success or failure depending on whether unwrapped object checks against the proof.
225
+
226
+ *Example*
189
227
 
190
228
  [source,ruby]
191
229
  ----
@@ -196,17 +234,29 @@ pipe Failure("Danger!"), check(%i[a b], :include?) # Failure "Danger!"
196
234
 
197
235
  ===== Fmap
198
236
 
199
- Allows you to unwrap a successful operation, make a modification, and rewrap the modification as a new success. This is a convenience wrapper to native {dry_monads_link} `#fmap` functionality. Example:
237
+ Allows you to unwrap a success, make a modification, and rewrap the modification as a new success. This is a convenience wrapper to native {dry_monads_link} `#fmap` functionality.
238
+
239
+ *I/O*
240
+
241
+ Processes and answers a success only.
242
+
243
+ *Example*
200
244
 
201
245
  [source,ruby]
202
246
  ----
203
- pipe %i[a b c], fmap { |input| input.join "-" } # Success "a-b-c"
204
- pipe Failure("Danger!"), fmap { |input| input.join "-" } # Failure "Danger!"
247
+ pipe %i[a b c], fmap { |object| object.join "-" } # Success "a-b-c"
248
+ pipe Failure("Danger!"), fmap { |object| object.join "-" } # Failure "Danger!"
205
249
  ----
206
250
 
207
251
  ===== Insert
208
252
 
209
- Allows you to insert an element after the input (default behavior) and wraps native link:https://rubyapi.org/o/array#method-i-insert[Array#insert] functionality. If the input is not an array, it will be cast as one. You can use the `:at` key to specify where you want insertion to happen. This step is most useful when needing to assemble arguments for passing to a subsequent step. Example:
253
+ Allows you to insert an element after an object (default behavior). This step wraps native link:https://rubyapi.org/o/array#method-i-insert[Array#insert] functionality. If the object is not an array, it will be cast as one. You can use the `:at` key to specify where you want insertion to happen. This step is most useful when needing to assemble arguments for passing to a subsequent step.
254
+
255
+ *I/O*
256
+
257
+ Processes and answers a success only.
258
+
259
+ *Example*
210
260
 
211
261
  [source,ruby]
212
262
  ----
@@ -218,7 +268,13 @@ pipe Failure("Danger!"), insert(:b) # Failure "Danger!"
218
268
 
219
269
  ===== Map
220
270
 
221
- Allows you to map over an enumerable and wraps native link:https://rubyapi.org/o/enumerable#method-i-map[Enumerable#map] functionality.
271
+ Allows you to map over an object (enumerable) by wrapping native link:https://rubyapi.org/o/enumerable#method-i-map[Enumerable#map] functionality.
272
+
273
+ *I/O*
274
+
275
+ Processes and answers a success only.
276
+
277
+ *Example*
222
278
 
223
279
  [source,ruby]
224
280
  ----
@@ -228,7 +284,13 @@ pipe Failure("Danger!"), map(&:inspect) # Failure "Danger!"
228
284
 
229
285
  ===== Merge
230
286
 
231
- Allows you to merge the input with additional attributes as a single hash. If the input is not a hash, then the input will be merged with the attributes using `step` as the key. The default `step` key can be renamed to a different key by using the `:as` key. Like the _Insert_ step, this is most useful when needing to assemble arguments and/or data for consumption by subsequent steps. Example:
287
+ Allows you to merge an object with additional attributes as a single hash. If the input is not a hash, then the object will be merged with `step` as the key. The default `step` key can be renamed to a different key by using the `:as` key. Like the _Insert_ step, this is most useful when assembling arguments and/or data for consumption by subsequent steps
288
+
289
+ *I/O*
290
+
291
+ Processes and answers a success only.
292
+
293
+ *Example*
232
294
 
233
295
  [source,ruby]
234
296
  ----
@@ -238,24 +300,15 @@ pipe "test", merge(as: :a, b: 2) # Success {a: "test", b: 2}
238
300
  pipe Failure("Danger!"), merge(b: 2) # Failure "Danger!"
239
301
  ----
240
302
 
241
- ===== Orr
242
-
243
- Allows you to operate on a failure and produce either a success or another failure. This is a convenience wrapper to native {dry_monads_link} `#or` functionality.
244
-
245
- ℹ️ Syntactically, `or` can't be used for this step since `or` is a native Ruby keyword so `orr` is used instead.
303
+ ===== Tee
246
304
 
247
- Example:
305
+ Allows you to run an operation and ignore the response while input is passed through as output. This behavior is similar in nature to the link:https://www.gnu.org/savannah-checkouts/gnu/gawk/manual/html_node/Tee-Program.html[tee] program in Bash.
248
306
 
249
- [source,ruby]
250
- ----
251
- pipe %i[a b c], orr { |input| Success input.join("-") } # Success [:a, :b, :c]
252
- pipe Failure("Danger!"), orr { Success "Resolved" } # Success "Resolved"
253
- pipe Failure("Danger!"), orr { |input| Failure "Big #{input}" } # Failure "Big Danger!"
254
- ----
307
+ *I/O*
255
308
 
256
- ===== Tee
309
+ Passes the result through while allowing you to execute arbitrary behavior.
257
310
 
258
- Allows you to run an operation and ignore the response while input is passed through as output. This behavior is similar in nature to the link:https://www.gnu.org/savannah-checkouts/gnu/gawk/manual/html_node/Tee-Program.html[tee] program in Bash. Example:
311
+ *Example*
259
312
 
260
313
  [source,ruby]
261
314
  ----
@@ -272,14 +325,20 @@ pipe Failure("Danger!"), tee(Kernel, :puts, "Example.")
272
325
 
273
326
  ===== To
274
327
 
275
- Allows you to delegate to an object -- which doesn't have a callable interface and may or may not answer a result -- for processing of input. If the response is not a monad, it'll be automatically wrapped as a `Success`. Example:
328
+ Allows you to delegate to an object which doesn't have a callable interface and may or may not answer a result. If the response is not a monad, it'll be automatically wrapped as a `Success`.
329
+
330
+ *I/O*
331
+
332
+ Processes a success only while sending the unwrapped object to the given object's corresponding method. The object is expected to answer either a plain Ruby object which will be automatically wrapped as a success or a {dry_monads_link} `Result`.
333
+
334
+ *Example*
276
335
 
277
336
  [source,ruby]
278
337
  ----
279
- Model = Struct.new :label, keyword_init: true do
338
+ Model = Struct.new :label do
280
339
  include Dry::Monads[:result]
281
340
 
282
- def self.for(...) = Success new(...)
341
+ def self.for(**) = Success new(**)
283
342
  end
284
343
 
285
344
  pipe({label: "Test"}, to(Model, :for)) # Success #<struct Model label="Test">
@@ -288,22 +347,42 @@ pipe Failure("Danger!"), to(Model, :for) # Failure "Danger!"
288
347
 
289
348
  ===== Try
290
349
 
291
- Allows you to try an operation which may fail while catching the exception as a failure for further processing. Example:
350
+ Allows you to try an operation which may fail while catching any exceptions as a failure for further processing. You can catch a single exception by providing the exception as a single value or multiple exceptions as an array of values.
351
+
352
+ *I/O*
353
+
354
+ Processes and answers a success only if there are no exceptions. Otherwise, captures any error as a failure.
355
+
356
+ *Example*
292
357
 
293
358
  [source,ruby]
294
359
  ----
295
- pipe "test", try(:to_json, catch: JSON::ParserError) # Success "\"test\""
296
- pipe "test", try(:invalid, catch: NoMethodError) # Failure "undefined method..."
297
- pipe Failure("Danger!"), try(:to_json, catch: JSON::ParserError) # Failure "Danger!"
360
+ pipe "test", try(:to_json, catch: JSON::ParserError)
361
+ # Success "\"test\""
362
+
363
+ pipe "test", try(:to_json, catch: [JSON::ParserError, StandardError])
364
+ # Success "\"test\""
365
+
366
+ pipe "test", try(:invalid, catch: NoMethodError)
367
+ # Failure(#<NoMethodError: undefined method `invalid' for an instance of String>)
368
+
369
+ pipe Failure("Danger!"), try(:to_json, catch: JSON::ParserError)
370
+ # Failure "Danger!"
298
371
  ----
299
372
 
300
373
  ===== Use
301
374
 
302
- Allows you to use another transaction which might have multiple steps of it's own, use an object that adheres to the {command_pattern_link}, or any function which answers a {dry_monads_link} `Result` object. In other words, you can use _use_ any object which responds to `#call` and answers a {dry_monads_link} `Result` object. This is great for chaining multiple transactions together.
375
+ Allows you to use another pipe to build a superpipe, use an object that adheres to the {command_pattern_link}, or any function which answers a {dry_monads_link} `Result` object. In other words, you can use _use_ any object which responds to `#call` that answers a {dry_monads_link} `Result` object. This is great for chaining multiple pipes together (i.e. superpipes).
376
+
377
+ *I/O*
378
+
379
+ Processes a success only while sending the unwrapped object to the command (or pipe) for further processing. A {dry_monads_link} `Result` is expected to be answered by the command.
380
+
381
+ *Example*
303
382
 
304
383
  [source,ruby]
305
384
  ----
306
- function = -> input { Success input * 3 }
385
+ function = -> number { Success number * 3 }
307
386
 
308
387
  pipe 3, use(function) # Success 9
309
388
  pipe Failure("Danger!"), use(function) # Failure "Danger!"
@@ -311,17 +390,30 @@ pipe Failure("Danger!"), use(function) # Failure "Danger!"
311
390
 
312
391
  ===== Validate
313
392
 
314
- Allows you to use an operation that will validate the input. This is especially useful when using {dry_schema_link}, {dry_validation_link}, or any operation that can respond to `#call` while answering a result that can be converted into a hash.
393
+ Allows you to use an contract for validating an object. This is especially useful when using {dry_schema_link}, {dry_validation_link}, or any contract that responds to `#call` and answers a `Result`.
394
+
395
+ 💡 Ensure you enable the {dry_monads_link} extension for {dry_schema_link} and/or {dry_validation_link} when using this step since this step expects the contract to respond to the `#to_monad` message.
396
+
397
+ By default, the `:as` key's value is `nil``. Use `:to_h`, for example, as the value for automatic casting to a `Hash`. You can also pass in any value to the `:as` key which is a valid method that the contract's result will respond to.
315
398
 
316
- By default, the `:as` key uses `:to_h` as it's value so you get automatic casting to a `Hash`. Use `nil`, as the value, to disable this behavior. You can also pass in any value to the `:as` key which is a valid method that the result will respond to.
399
+ *I/O*
400
+
401
+ Processes a success only. A success will be rewrapped as a success if the `:as` keyword is supplied. Otherwise, any failure is immediately passed through.
402
+
403
+ *Example*
317
404
 
318
405
  [source,ruby]
319
406
  ----
320
407
  schema = Dry::Schema.Params { required(:label).filled :string }
321
408
 
322
- pipe({label: "Test"}, validate(schema)) # Success label: "Test"
323
- pipe({label: "Test"}, validate(schema, as: nil)) # Success #<Dry::Schema::Result{:label=>"Test"} errors={} path=[]>
324
- pipe Failure("Danger!"), validate(schema) # Failure "Danger!"
409
+ pipe({label: "Test"}, validate(schema))
410
+ # Success label: "Test"
411
+
412
+ pipe({label: "Test"}, validate(schema, as: nil))
413
+ # Success #<Dry::Schema::Result{:label=>"Test"} errors={} path=[]>
414
+
415
+ pipe Failure("Danger!"), validate(schema)
416
+ # Failure "Danger!"
325
417
  ----
326
418
 
327
419
  ==== Advanced
@@ -334,8 +426,8 @@ You can always use a `Proc` as a custom step. Example:
334
426
 
335
427
  [source,ruby]
336
428
  ----
337
- include Pipeable
338
429
  include Dry::Monads[:result]
430
+ include Pipeable
339
431
 
340
432
  pipe :a,
341
433
  insert(:b),
@@ -355,7 +447,7 @@ include Pipeable
355
447
 
356
448
  pipe :a,
357
449
  insert(:b),
358
- -> result { result.fmap { |input| input.join "_" } },
450
+ -> result { result.fmap { |items| items.join "_" } },
359
451
  as(:to_sym)
360
452
 
361
453
  # Yields: Success :a_b
@@ -363,35 +455,30 @@ pipe :a,
363
455
 
364
456
  ===== Methods
365
457
 
366
- Methods -- in addition to procs and lambdas -- are the _preferred_ way to add custom steps due to the concise syntax. Example:
458
+ Methods, in addition to procs and lambdas, are the _preferred_ way to add custom steps due to the concise syntax. Example:
367
459
 
368
460
  [source,ruby]
369
461
  ----
370
462
  class Demo
371
463
  include Pipeable
372
464
 
373
- def call input
374
- pipe :a,
375
- insert(:b),
376
- :join,
377
- as(:to_sym)
378
- end
465
+ def call(input) = pipe input, insert(:b), :join, as(:to_sym)
379
466
 
380
467
  private
381
468
 
382
- def join(result) = result.fmap { |input| input.join "_" }
469
+ def join(result) = result.fmap { |items| items.join "_" }
383
470
  end
384
471
 
385
- Demo.new.call :a # Yields: Success :a_b
472
+ Demo.new.call :a # Success :a_b
386
473
  ----
387
474
 
388
- All methods can be referenced by symbol as shown via `:join` above. Using a symbol is syntactic sugar for link:https://rubyapi.org/o/object#method-i-method[Object#method] so the use of the `:join` symbol is the same as using `method(:join)`. Both work but the former requires less typing than the latter.
475
+ All methods can be referenced by symbol as shown via `:join` above. Using a symbol is syntactic sugar for link:https://rubyapi.org/o/object#method-i-method[Object#method] so `:join` (symbol) is the same as using `method(:join)`. Both work but the former requires less typing.
389
476
 
390
477
  ===== Custom
391
478
 
392
479
  If you'd like to define permanent and reusable steps, you can register a custom step which requires you to:
393
480
 
394
- . Define a custom step as a new class.
481
+ . Define a custom step as a class, lambda, or proc.
395
482
  . Register your custom step along side the existing default steps.
396
483
 
397
484
  Here's what this would look like:
@@ -405,7 +492,7 @@ module CustomSteps
405
492
  @delimiter = delimiter
406
493
  end
407
494
 
408
- def call(result) = result.fmap { |input| input.join delimiter }
495
+ def call(result) = result.fmap { |items| items.join delimiter }
409
496
 
410
497
  private
411
498
 
@@ -418,12 +505,82 @@ Pipeable::Steps::Container.register :join, CustomSteps::Join
418
505
  include Pipeable
419
506
 
420
507
  pipe :a, insert(:b), join, as(:to_sym)
421
- # Yields: Success :a_b
508
+ # Success :a_b
422
509
 
423
510
  pipe :a, insert(:b), join(""), as(:to_sym)
424
- # Yields: Success :ab
511
+ # Success :ab
512
+ ----
513
+
514
+ A lambda or proc can be used too (albeit in limited capacity). Here's a version of the above using a lambda:
515
+
516
+ [source,ruby]
517
+ ----
518
+ module CustomSteps
519
+ Join = -> result { result.fmap { |items| items.join "_" } }
520
+ end
521
+
522
+ Pipeable::Steps::Container.register :join, CustomSteps::Join
523
+
524
+ include Pipeable
525
+
526
+ puts pipe(:a, insert(:b), join, as(:to_sym))
527
+ # Success :a_b
425
528
  ----
426
529
 
530
+ === Superpipes
531
+
532
+ Superpipes, as first hinted at in the `use` step above, are a combination of _pipeable_ objects chained together as individual steps. This allows you to reuse existing pipeable objects in new and interesting ways. Here's an contrived, but simple, example of what a superpipe looks like when built from pipeable objects:
533
+
534
+ [source,ruby]
535
+ ----
536
+ class One
537
+ include Pipeable
538
+
539
+ def initialize label = "one"
540
+ @label = label
541
+ end
542
+
543
+ def call(item) = pipe item, insert(label, at: 0)
544
+
545
+ private
546
+
547
+ attr_reader :label
548
+ end
549
+
550
+ class Two
551
+ include Pipeable
552
+
553
+ def initialize label = "two"
554
+ @label = label
555
+ end
556
+
557
+ def call(item) = pipe item, insert(label)
558
+
559
+ private
560
+
561
+ attr_reader :label
562
+ end
563
+
564
+ class Three
565
+ include Pipeable
566
+
567
+ def initialize one: One.new, two: Two.new
568
+ @one = one
569
+ @two = two
570
+ end
571
+
572
+ def call(item) = pipe item, use(one), use(two)
573
+
574
+ private
575
+
576
+ attr_reader :one, :two
577
+ end
578
+ ----
579
+
580
+ Notice, `One` and `Two` are standard pipeable objects with their own individual steps while `Three` injects both `One` and `Two` as dependencies and then subsequently pipes them together within it's own `#call` method via the `use` step. This is essence of a superpipe. ...and, yes, a superpipe can be an individual step too in some other object. Turtles all the way down (or up). 😉
581
+
582
+ Again, the above is contrived but hopefully this illustrates how you can build more complex architectures from smaller pipes.
583
+
427
584
  === Containers
428
585
 
429
586
  Should you not want the basic steps, need custom steps, or a hybrid of default and custom steps, you can define your own container and provide it as an argument to `.[]` when including pipeable behavior. Example:
@@ -448,7 +605,7 @@ pipe :a, echo, insert(:b)
448
605
 
449
606
  The above is a hybrid example where the `CustomContainer` registers a custom `echo` step along with the default `insert` step to make a new container. This is included when passed in as an argument via `.[]` (i.e. `include Pipeable[CustomContainer]`).
450
607
 
451
- Whether you use default, custom, or hybrid steps, you have maximum flexibility using this approach.
608
+ Whether you use default, custom, or hybrid steps, you have maximum flexibility when using containers.
452
609
 
453
610
  === Composition
454
611
 
@@ -483,14 +640,14 @@ The architecture of this gem is built on top of the following concepts and gems:
483
640
 
484
641
  === Style Guide
485
642
 
486
- * *Transactions*
487
- ** Use a single method (i.e. `#call`) which is public and adheres to the {command_pattern_link} so transactions can be piped together if desired.
643
+ * *Pipes*
644
+ ** Use a single method (i.e. `#call`) which is public and adheres to the {command_pattern_link} so multiple pipes can be piped together (i.e. superpipes) if desired.
488
645
  * *Steps*
489
646
  ** Inherit from the `Abstract` class to gain monad, composition, and dependency behavior. This allows subclasses to have direct access to the base positional, keyword, and block arguments. These variables are prefixed with `base_*` in order to not conflict with subclasses which might only want to use non-prefixed variables for convenience.
490
- ** All filtered arguments -- in other words, the unused arguments -- need to be passed up to the superclass from the subclass (i.e. `super(*positionals, **keywords, &block)`). Doing so allows the superclass (i.e. `Abstract`) to provide access to `base_positionals`, `base_keywords`, and `base_block` for use if desired by the subclass.
647
+ ** All filtered arguments -- in other words, unused arguments -- need to be passed up to the superclass from the subclass (i.e. `super(*positionals, **keywords, &block)`). Doing so allows the superclass (i.e. `Abstract`) to provide access to `base_positionals`, `base_keywords`, and `base_block` for use if desired by the subclass.
491
648
  ** The `#call` method must define a single positional `result` parameter since a monad will be passed as an argument. Example: `def call(result) = # Implementation`.
492
- ** Each block within the `#call` method should use the `input` parameter to be consistent. More specific parameters like `argument` or `operation` should be used to improve readability when possible. Example: `def call(result) = result.bind { |input| # Implementation }`.
493
- ** Use implicit blocks sparingly. Most of the default steps shy away from using blocks because it can make the code more complex. Use private methods, custom steps, and/or separate transactions if the code becomes too complex because you might have a smaller object which needs extraction.
649
+ ** Each block within the `#call` method should use the `object` parameter to be consistent. More specific parameters like `operation` or `contract` should be used to improve readability when context allows. Example: `def call(result) = result.bind { |object| # Implementation }`.
650
+ ** Use implicit blocks sparingly. Most of the default steps shy away from using blocks because the code becomes more complex. Use private methods, custom steps, and/or separate pipes if the code becomes too complex because you might have a smaller object which needs extraction.
494
651
 
495
652
  === Debugging
496
653
 
@@ -508,7 +665,7 @@ The above breakpoint will allow you inspect the result of the `#check` step and/
508
665
 
509
666
  === Troubleshooting
510
667
 
511
- The following might be of aid to as you implement your own transactions.
668
+ The following might be of aid to as you implement your own pipes.
512
669
 
513
670
  ==== Type Errors
514
671
 
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "dry/monads"
4
- require "marameters"
5
4
 
6
5
  module Pipeable
7
6
  module Steps
@@ -14,12 +13,11 @@ module Pipeable
14
13
  @base_positionals = positionals
15
14
  @base_keywords = keywords
16
15
  @base_block = block
17
- @marameters = Marameters
18
16
  end
19
17
 
20
18
  protected
21
19
 
22
- attr_reader :base_positionals, :base_keywords, :base_block, :marameters
20
+ attr_reader :base_positionals, :base_keywords, :base_block
23
21
  end
24
22
  end
25
23
  end
@@ -2,10 +2,10 @@
2
2
 
3
3
  module Pipeable
4
4
  module Steps
5
- # Allows result to be messaged as a callable.
5
+ # Messages object, with optional arguments, as different result.
6
6
  class As < Abstract
7
7
  def call result
8
- result.fmap { |operation| operation.public_send(*base_positionals, **base_keywords) }
8
+ result.fmap { |object| object.public_send(*base_positionals, **base_keywords) }
9
9
  end
10
10
  end
11
11
  end
@@ -4,7 +4,7 @@ module Pipeable
4
4
  module Steps
5
5
  # Wraps Dry Monads `#bind` method as a step.
6
6
  class Bind < Abstract
7
- def call(result) = result.bind { |input| base_block.call input }
7
+ def call(result) = result.bind { |object| base_block.call object }
8
8
  end
9
9
  end
10
10
  end
@@ -1,29 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "marameters"
4
+
3
5
  module Pipeable
4
6
  module Steps
5
- # Checks if operation is true and then answers success (passthrough) or failure (with argument).
7
+ # Checks if proof is true and answers success (passthrough) or failure (with optional argument).
6
8
  class Check < Abstract
7
- def initialize(operation, message, **)
8
- super(**)
9
- @operation = operation
9
+ def initialize proof, message
10
+ super()
11
+ @proof = proof
10
12
  @message = message
11
13
  end
12
14
 
13
15
  def call result
14
- result.bind do |arguments|
15
- answer = question arguments
16
- answer == true || answer.is_a?(Success) ? result : Failure(arguments)
16
+ result.bind do |object|
17
+ answer = question object
18
+ answer == true || answer.is_a?(Success) ? result : Failure(object)
17
19
  end
18
20
  end
19
21
 
20
22
  private
21
23
 
22
- attr_reader :operation, :message
24
+ attr_reader :proof, :message
23
25
 
24
- def question arguments
25
- splat = marameters.categorize operation.method(message).parameters, arguments
26
- operation.public_send(message, *splat.positionals, **splat.keywords, &splat.block)
26
+ def question object
27
+ splat = Marameters.categorize proof.method(message).parameters, object
28
+ proof.public_send(message, *splat.positionals, **splat.keywords, &splat.block)
27
29
  end
28
30
  end
29
31
  end
@@ -8,6 +8,7 @@ module Pipeable
8
8
  module Container
9
9
  extend Containable
10
10
 
11
+ register :alt, Or
11
12
  register :as, As
12
13
  register :bind, Bind
13
14
  register :check, Check
@@ -15,7 +16,6 @@ module Pipeable
15
16
  register :insert, Insert
16
17
  register :map, Map
17
18
  register :merge, Merge
18
- register :orr, Or
19
19
  register :tee, Tee
20
20
  register :to, To
21
21
  register :try, Try
@@ -4,7 +4,7 @@ module Pipeable
4
4
  module Steps
5
5
  # Wraps Dry Monads `#fmap` method as a step.
6
6
  class Fmap < Abstract
7
- def call(result) = result.fmap { |input| base_block.call input }
7
+ def call(result) = result.fmap { |object| base_block.call object }
8
8
  end
9
9
  end
10
10
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Pipeable
4
4
  module Steps
5
- # Inserts elements before, after, or around input.
5
+ # Inserts elements before or after an object.
6
6
  class Insert < Abstract
7
7
  LAST = -1
8
8
 
@@ -13,8 +13,8 @@ module Pipeable
13
13
  end
14
14
 
15
15
  def call result
16
- result.fmap do |input|
17
- cast = input.is_a?(Array) ? input : [input]
16
+ result.fmap do |object|
17
+ cast = object.is_a?(Array) ? object : [object]
18
18
  value.is_a?(Array) ? cast.insert(at, *value) : cast.insert(at, value)
19
19
  end
20
20
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Pipeable
4
4
  module Steps
5
- # Maps over a collection, processing each element, and answering a new result.
5
+ # Maps over an enumerable, processes each element, and answers a new enumerable.
6
6
  class Map < Abstract
7
7
  def call(result) = result.fmap { |collection| collection.map(&base_block) }
8
8
  end
@@ -2,19 +2,19 @@
2
2
 
3
3
  module Pipeable
4
4
  module Steps
5
- # Merges initialized attributes with step argument for use by a subsequent step.
5
+ # Merges initialized attributes with step object for use by subsequent step.
6
6
  class Merge < Abstract
7
- def initialize as: :step, **keywords
8
- super(**keywords)
7
+ def initialize(as: :step, **)
8
+ super(**)
9
9
  @as = as
10
10
  end
11
11
 
12
12
  def call result
13
- result.fmap do |input|
14
- if input.is_a? Hash
15
- input.merge! base_keywords
13
+ result.fmap do |object|
14
+ if object.is_a? Hash
15
+ object.merge! base_keywords
16
16
  else
17
- {as => input}.merge!(base_keywords)
17
+ {as => object}.merge!(base_keywords)
18
18
  end
19
19
  end
20
20
  end
@@ -4,7 +4,7 @@ module Pipeable
4
4
  module Steps
5
5
  # Wraps Dry Monads `#or` method as a step.
6
6
  class Or < Abstract
7
- def call(result) = result.or { |input| base_block.call input }
7
+ def call(result) = result.or { |object| base_block.call object }
8
8
  end
9
9
  end
10
10
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Pipeable
4
4
  module Steps
5
- # Messages operation, without any response checks, while passing input through as output.
5
+ # Messages operation, without any checks, while passing input through as output.
6
6
  class Tee < Abstract
7
7
  def initialize(operation, *, **)
8
8
  super(*, **)
@@ -1,25 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "marameters"
4
+
3
5
  module Pipeable
4
6
  module Steps
5
- # Delegates to a non-callable operation which automatically wraps the result if necessary.
7
+ # Delegates to a non-callable object which automatically wraps the result if necessary.
6
8
  class To < Abstract
7
- def initialize(operation, message, **)
9
+ def initialize(object, message, **)
8
10
  super(**)
9
- @operation = operation
11
+ @object = object
10
12
  @message = message
11
13
  end
12
14
 
13
15
  def call result
14
16
  result.bind do |arguments|
15
- splat = marameters.categorize operation.method(message).parameters, arguments
16
- wrap operation.public_send(message, *splat.positionals, **splat.keywords, &splat.block)
17
+ splat = Marameters.categorize object.method(message).parameters, arguments
18
+ wrap object.public_send(message, *splat.positionals, **splat.keywords, &splat.block)
17
19
  end
18
20
  end
19
21
 
20
22
  private
21
23
 
22
- attr_reader :operation, :message
24
+ attr_reader :object, :message
23
25
 
24
26
  def wrap(result) = result.is_a?(Dry::Monads::Result) ? result : Success(result)
25
27
  end
@@ -2,17 +2,17 @@
2
2
 
3
3
  module Pipeable
4
4
  module Steps
5
- # Messages a risky operation which may pass or fail.
5
+ # Sends a risky message to an object which may pass or fail.
6
6
  class Try < Abstract
7
- def initialize *positionals, catch:, **keywords
8
- super(*positionals, **keywords)
7
+ def initialize(*, catch:, **)
8
+ super(*, **)
9
9
  @catch = catch
10
10
  end
11
11
 
12
12
  def call result
13
- result.fmap { |operation| operation.public_send(*base_positionals, **base_keywords) }
13
+ result.fmap { |object| object.public_send(*base_positionals, **base_keywords) }
14
14
  rescue *Array(catch) => error
15
- Failure error.message
15
+ Failure error
16
16
  end
17
17
 
18
18
  private
@@ -2,18 +2,18 @@
2
2
 
3
3
  module Pipeable
4
4
  module Steps
5
- # Use another transaction -- or any command -- which answers a result.
5
+ # Messages a command (or pipe) which answers a result.
6
6
  class Use < Abstract
7
- def initialize(operation, **)
7
+ def initialize(command, **)
8
8
  super(**)
9
- @operation = operation
9
+ @command = command
10
10
  end
11
11
 
12
- def call(result) = result.bind { |input| operation.call input }
12
+ def call(result) = result.bind { |input| command.call input }
13
13
 
14
14
  private
15
15
 
16
- attr_reader :operation
16
+ attr_reader :command
17
17
  end
18
18
  end
19
19
  end
@@ -2,27 +2,23 @@
2
2
 
3
3
  module Pipeable
4
4
  module Steps
5
- # Validates a result via a callable operation.
5
+ # Validates result via a callable contract.
6
6
  class Validate < Abstract
7
- def initialize(operation, as: :to_h, **)
8
- super(**)
9
- @operation = operation
7
+ def initialize contract, as: nil
8
+ super()
9
+ @contract = contract
10
10
  @as = as
11
11
  end
12
12
 
13
- def call result
14
- result.bind do |payload|
15
- value = operation.call payload
16
-
17
- return Failure value if value.failure?
18
-
19
- Success(as ? value.public_send(as) : value)
20
- end
21
- end
13
+ def call(result) = result.bind { |payload| cast payload }
22
14
 
23
15
  private
24
16
 
25
- attr_reader :operation, :as
17
+ attr_reader :contract, :as
18
+
19
+ def cast payload
20
+ contract.call(payload).to_monad.fmap { |data| as ? data.public_send(as) : data }
21
+ end
26
22
  end
27
23
  end
28
24
  end
data/pipeable.gemspec CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |spec|
4
4
  spec.name = "pipeable"
5
- spec.version = "0.4.0"
5
+ spec.version = "0.5.0"
6
6
  spec.authors = ["Brooke Kuhlmann"]
7
7
  spec.email = ["brooke@alchemists.io"]
8
8
  spec.homepage = "https://alchemists.io/projects/pipeable"
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pipeable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brooke Kuhlmann
@@ -35,7 +35,7 @@ cert_chain:
35
35
  3n5C8/6Zh9DYTkpcwPSuIfAga6wf4nXc9m6JAw8AuMLaiWN/r/2s4zJsUHYERJEu
36
36
  gZGm4JqtuSg8pYjPeIJxS960owq+SfuC+jxqmRA54BisFCv/0VOJi7tiJVY=
37
37
  -----END CERTIFICATE-----
38
- date: 2024-04-30 00:00:00.000000000 Z
38
+ date: 2024-05-07 00:00:00.000000000 Z
39
39
  dependencies:
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: containable
@@ -164,7 +164,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
164
164
  - !ruby/object:Gem::Version
165
165
  version: '0'
166
166
  requirements: []
167
- rubygems_version: 3.5.9
167
+ rubygems_version: 3.5.10
168
168
  signing_key:
169
169
  specification_version: 4
170
170
  summary: A domain specific language for building functionally composable steps.
metadata.gz.sig CHANGED
Binary file