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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/README.adoc +227 -70
- data/lib/pipeable/steps/abstract.rb +1 -3
- data/lib/pipeable/steps/as.rb +2 -2
- data/lib/pipeable/steps/bind.rb +1 -1
- data/lib/pipeable/steps/check.rb +13 -11
- data/lib/pipeable/steps/container.rb +1 -1
- data/lib/pipeable/steps/fmap.rb +1 -1
- data/lib/pipeable/steps/insert.rb +3 -3
- data/lib/pipeable/steps/map.rb +1 -1
- data/lib/pipeable/steps/merge.rb +7 -7
- data/lib/pipeable/steps/or.rb +1 -1
- data/lib/pipeable/steps/tee.rb +1 -1
- data/lib/pipeable/steps/to.rb +8 -6
- data/lib/pipeable/steps/try.rb +5 -5
- data/lib/pipeable/steps/use.rb +5 -5
- data/lib/pipeable/steps/validate.rb +10 -14
- data/pipeable.gemspec +1 -1
- data.tar.gz.sig +0 -0
- metadata +3 -3
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2467c8bb8dda8efd0a2e4fc5a78765f8abaeab65c288c380d9c7358244ba5aaf
|
4
|
+
data.tar.gz: 96c96e5e12d25f81dd9c7e83b59850691036513b8456acab4868b93a71083f22
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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 { |
|
182
|
-
pipe %i[a b c], bind { |
|
183
|
-
pipe Failure("Danger!"), bind { |
|
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
|
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
|
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 { |
|
204
|
-
pipe Failure("Danger!"), fmap { |
|
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
|
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
|
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
|
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
|
-
=====
|
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
|
-
|
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
|
-
|
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
|
-
|
309
|
+
Passes the result through while allowing you to execute arbitrary behavior.
|
257
310
|
|
258
|
-
|
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
|
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
|
338
|
+
Model = Struct.new :label do
|
280
339
|
include Dry::Monads[:result]
|
281
340
|
|
282
|
-
def self.for(
|
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
|
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)
|
296
|
-
|
297
|
-
|
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
|
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 = ->
|
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
|
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
|
-
|
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))
|
323
|
-
|
324
|
-
|
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 { |
|
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
|
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 { |
|
469
|
+
def join(result) = result.fmap { |items| items.join "_" }
|
383
470
|
end
|
384
471
|
|
385
|
-
Demo.new.call :a #
|
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
|
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
|
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 { |
|
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
|
-
#
|
508
|
+
# Success :a_b
|
422
509
|
|
423
510
|
pipe :a, insert(:b), join(""), as(:to_sym)
|
424
|
-
#
|
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
|
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
|
-
* *
|
487
|
-
** Use a single method (i.e. `#call`) which is public and adheres to the {command_pattern_link} so
|
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,
|
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 `
|
493
|
-
** Use implicit blocks sparingly. Most of the default steps shy away from using blocks because
|
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
|
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
|
20
|
+
attr_reader :base_positionals, :base_keywords, :base_block
|
23
21
|
end
|
24
22
|
end
|
25
23
|
end
|
data/lib/pipeable/steps/as.rb
CHANGED
@@ -2,10 +2,10 @@
|
|
2
2
|
|
3
3
|
module Pipeable
|
4
4
|
module Steps
|
5
|
-
#
|
5
|
+
# Messages object, with optional arguments, as different result.
|
6
6
|
class As < Abstract
|
7
7
|
def call result
|
8
|
-
result.fmap { |
|
8
|
+
result.fmap { |object| object.public_send(*base_positionals, **base_keywords) }
|
9
9
|
end
|
10
10
|
end
|
11
11
|
end
|
data/lib/pipeable/steps/bind.rb
CHANGED
data/lib/pipeable/steps/check.rb
CHANGED
@@ -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
|
7
|
+
# Checks if proof is true and answers success (passthrough) or failure (with optional argument).
|
6
8
|
class Check < Abstract
|
7
|
-
def initialize
|
8
|
-
super(
|
9
|
-
@
|
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 |
|
15
|
-
answer = question
|
16
|
-
answer == true || answer.is_a?(Success) ? result : Failure(
|
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 :
|
24
|
+
attr_reader :proof, :message
|
23
25
|
|
24
|
-
def question
|
25
|
-
splat =
|
26
|
-
|
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
|
data/lib/pipeable/steps/fmap.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
module Pipeable
|
4
4
|
module Steps
|
5
|
-
# Inserts elements before
|
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 |
|
17
|
-
cast =
|
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
|
data/lib/pipeable/steps/map.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
module Pipeable
|
4
4
|
module Steps
|
5
|
-
# Maps over
|
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
|
data/lib/pipeable/steps/merge.rb
CHANGED
@@ -2,19 +2,19 @@
|
|
2
2
|
|
3
3
|
module Pipeable
|
4
4
|
module Steps
|
5
|
-
# Merges initialized attributes with step
|
5
|
+
# Merges initialized attributes with step object for use by subsequent step.
|
6
6
|
class Merge < Abstract
|
7
|
-
def initialize
|
8
|
-
super(**
|
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 |
|
14
|
-
if
|
15
|
-
|
13
|
+
result.fmap do |object|
|
14
|
+
if object.is_a? Hash
|
15
|
+
object.merge! base_keywords
|
16
16
|
else
|
17
|
-
{as =>
|
17
|
+
{as => object}.merge!(base_keywords)
|
18
18
|
end
|
19
19
|
end
|
20
20
|
end
|
data/lib/pipeable/steps/or.rb
CHANGED
data/lib/pipeable/steps/tee.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
module Pipeable
|
4
4
|
module Steps
|
5
|
-
# Messages operation, without any
|
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(*, **)
|
data/lib/pipeable/steps/to.rb
CHANGED
@@ -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
|
7
|
+
# Delegates to a non-callable object which automatically wraps the result if necessary.
|
6
8
|
class To < Abstract
|
7
|
-
def initialize(
|
9
|
+
def initialize(object, message, **)
|
8
10
|
super(**)
|
9
|
-
@
|
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 =
|
16
|
-
wrap
|
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 :
|
24
|
+
attr_reader :object, :message
|
23
25
|
|
24
26
|
def wrap(result) = result.is_a?(Dry::Monads::Result) ? result : Success(result)
|
25
27
|
end
|
data/lib/pipeable/steps/try.rb
CHANGED
@@ -2,17 +2,17 @@
|
|
2
2
|
|
3
3
|
module Pipeable
|
4
4
|
module Steps
|
5
|
-
#
|
5
|
+
# Sends a risky message to an object which may pass or fail.
|
6
6
|
class Try < Abstract
|
7
|
-
def initialize
|
8
|
-
super(
|
7
|
+
def initialize(*, catch:, **)
|
8
|
+
super(*, **)
|
9
9
|
@catch = catch
|
10
10
|
end
|
11
11
|
|
12
12
|
def call result
|
13
|
-
result.fmap { |
|
13
|
+
result.fmap { |object| object.public_send(*base_positionals, **base_keywords) }
|
14
14
|
rescue *Array(catch) => error
|
15
|
-
Failure error
|
15
|
+
Failure error
|
16
16
|
end
|
17
17
|
|
18
18
|
private
|
data/lib/pipeable/steps/use.rb
CHANGED
@@ -2,18 +2,18 @@
|
|
2
2
|
|
3
3
|
module Pipeable
|
4
4
|
module Steps
|
5
|
-
#
|
5
|
+
# Messages a command (or pipe) which answers a result.
|
6
6
|
class Use < Abstract
|
7
|
-
def initialize(
|
7
|
+
def initialize(command, **)
|
8
8
|
super(**)
|
9
|
-
@
|
9
|
+
@command = command
|
10
10
|
end
|
11
11
|
|
12
|
-
def call(result) = result.bind { |input|
|
12
|
+
def call(result) = result.bind { |input| command.call input }
|
13
13
|
|
14
14
|
private
|
15
15
|
|
16
|
-
attr_reader :
|
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
|
5
|
+
# Validates result via a callable contract.
|
6
6
|
class Validate < Abstract
|
7
|
-
def initialize
|
8
|
-
super(
|
9
|
-
@
|
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 :
|
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
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
|
+
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-
|
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.
|
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
|