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 +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
|