pipeable 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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