pipeable 0.4.0 → 0.6.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 +215 -93
- data/lib/pipeable/steps/abstract.rb +1 -3
- data/lib/pipeable/steps/amap.rb +10 -0
- 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 +2 -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 +4 -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: 4c03f09d3e8268f1786faa5907fe4f9dbc4beaf02c874bec73b683fd1e4a51de
|
4
|
+
data.tar.gz: 29943bc0fed70a2661cee558e5a4d23090af2fe216ba085ec1ff00e3da435ea3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ebaeb186fb3c672501fa3b5414b2d4fe381a45fa22004ae2f128b80abc15d012f0ff70befb900ad75d82b760af33fa4a35c7db5d3113e7f3f261782f01e7dd24
|
7
|
+
data.tar.gz: 41fe4361d2a0226ceeb8796d1edc9e6fd0e19bc00539c3d3c4e87c57d09a871818d051d7a8d881b294c0f3ea8e73f2575a0ead5dcaaf3d44dbbb0dd53065d749
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data/README.adoc
CHANGED
@@ -10,17 +10,18 @@
|
|
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
|
|
21
22
|
== Features
|
22
23
|
|
23
|
-
* Built atop
|
24
|
+
* Built atop native {function_composition_link}.
|
24
25
|
* Adheres to the {railway_pattern_link}.
|
25
26
|
* Provides built-in and customizable domain-specific steps.
|
26
27
|
* Provides chainable _pipes_ which can be used to build more complex workflows.
|
@@ -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
|
|
@@ -130,7 +131,7 @@ pipe(input, *steps)
|
|
130
131
|
|
131
132
|
The first argument is your input which can be a Ruby primitive or a monad. Regardless, the input will be automatically wrapped as a `Success` -- but only if not a `Result` to begin with -- before passing to the first step. From there, all steps are _required_ to answer a monad in order to adhere to the {railway_pattern_link}.
|
132
133
|
|
133
|
-
Behind the scenes, the `#pipe` method is syntactic sugar
|
134
|
+
Behind the scenes, the `#pipe` method is syntactic sugar built atop {function_composition_link} which means if this code were to be rewritten:
|
134
135
|
|
135
136
|
[source,ruby]
|
136
137
|
----
|
@@ -140,7 +141,7 @@ pipe csv,
|
|
140
141
|
map { |item| "#{item[:book]}: #{item[:price]}" }
|
141
142
|
----
|
142
143
|
|
143
|
-
|
144
|
+
...then the above would look like the following (as rewritten in native Ruby):
|
144
145
|
|
145
146
|
[source,ruby]
|
146
147
|
----
|
@@ -151,19 +152,53 @@ Then the above would look like this using native Ruby:
|
|
151
152
|
).call Success(csv)
|
152
153
|
----
|
153
154
|
|
154
|
-
|
155
|
+
Visually, the pipe can be diagramed as follows:
|
156
|
+
|
157
|
+
image::https://alchemists.io/images/projects/pipeable/diagrams/pipe.png[A diagram of pipe steps,width=591,height=734,role=focal_point]
|
158
|
+
|
159
|
+
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
160
|
|
156
161
|
=== Steps
|
157
162
|
|
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
|
163
|
+
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). Each step can be initialized and called:
|
164
|
+
|
165
|
+
* `+#initialize+`: Arguments vary per step but can be positional, keyword, and/or block arguments. This is how you _customize_ the behavior of each step.
|
166
|
+
* `+#call+`: Expects a {dry_monads_link} `Result` object as input. The output is 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 (as detailed below).
|
159
167
|
|
160
168
|
==== Basic
|
161
169
|
|
162
|
-
The following are the basic (default) steps for building for
|
170
|
+
The following are the basic (default) steps for building custom pipes for which you can mix and match within your own implementation.
|
171
|
+
|
172
|
+
===== alt
|
173
|
+
|
174
|
+
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.
|
175
|
+
|
176
|
+
Accepts a failure while answering either a success or failure. Example:
|
177
|
+
|
178
|
+
[source,ruby]
|
179
|
+
----
|
180
|
+
pipe %i[a b c], alt { |object| Success object.join("-") } # Success [:a, :b, :c]
|
181
|
+
pipe Failure("Danger!"), alt { Success "Resolved" } # Success "Resolved"
|
182
|
+
pipe Failure("Danger!"), alt { |object| Failure "Big #{object}" } # Failure "Big Danger!"
|
183
|
+
----
|
184
|
+
|
185
|
+
===== amap
|
163
186
|
|
164
|
-
|
187
|
+
Allows you to unwrap a failure, make a modification, and wrap the modification as a new failure. This is a convenience wrapper to native {dry_monads_link} `#alt_map` functionality.
|
165
188
|
|
166
|
-
|
189
|
+
Accepts and answers a failure. Example:
|
190
|
+
|
191
|
+
[source,ruby]
|
192
|
+
----
|
193
|
+
pipe Failure("Danger"), amap { |object| "#{object}!" } # Failure "Danger!"
|
194
|
+
pipe Success("Pass"), amap { |object| "#{object}!" } # Success "Pass"
|
195
|
+
----
|
196
|
+
|
197
|
+
===== as
|
198
|
+
|
199
|
+
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.
|
200
|
+
|
201
|
+
Accepts and answers a success. Example:
|
167
202
|
|
168
203
|
[source,ruby]
|
169
204
|
----
|
@@ -172,20 +207,24 @@ pipe %i[a b c], as(:dig, 1) # Success :b
|
|
172
207
|
pipe Failure("Danger!"), as(:inspect) # Failure "Danger!"
|
173
208
|
----
|
174
209
|
|
175
|
-
=====
|
210
|
+
===== bind
|
211
|
+
|
212
|
+
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.
|
176
213
|
|
177
|
-
|
214
|
+
Accepts a success while answering either a success or failure. Example:
|
178
215
|
|
179
216
|
[source,ruby]
|
180
217
|
----
|
181
|
-
pipe %i[a b c], bind { |
|
182
|
-
pipe %i[a b c], bind { |
|
183
|
-
pipe Failure("Danger!"), bind { |
|
218
|
+
pipe %i[a b c], bind { |object| Success object.join("-") } # Success "a-b-c"
|
219
|
+
pipe %i[a b c], bind { |object| Failure object } # Failure [:a, :b, :c]
|
220
|
+
pipe Failure("Danger!"), bind { |object| Success object.join("-") } # Failure "Danger!"
|
184
221
|
----
|
185
222
|
|
186
|
-
=====
|
223
|
+
===== check
|
224
|
+
|
225
|
+
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`.
|
187
226
|
|
188
|
-
|
227
|
+
Accepts a success while answering a success or failure depending on whether unwrapped object checks against the proof. Example:
|
189
228
|
|
190
229
|
[source,ruby]
|
191
230
|
----
|
@@ -194,19 +233,23 @@ pipe :a, check(%i[b c], :include?) # Failure :a
|
|
194
233
|
pipe Failure("Danger!"), check(%i[a b], :include?) # Failure "Danger!"
|
195
234
|
----
|
196
235
|
|
197
|
-
=====
|
236
|
+
===== fmap
|
198
237
|
|
199
|
-
Allows you to unwrap a
|
238
|
+
Allows you to unwrap a success, make a modification, and wrap the modification as a new success. This is a convenience wrapper to native {dry_monads_link} `#fmap` functionality.
|
239
|
+
|
240
|
+
Accepts and answers a success. Example:
|
200
241
|
|
201
242
|
[source,ruby]
|
202
243
|
----
|
203
|
-
pipe %i[a b c], fmap { |
|
204
|
-
pipe Failure("Danger!"), fmap { |
|
244
|
+
pipe %i[a b c], fmap { |object| object.join "-" } # Success "a-b-c"
|
245
|
+
pipe Failure("Danger!"), fmap { |object| object.join "-" } # Failure "Danger!"
|
205
246
|
----
|
206
247
|
|
207
|
-
=====
|
248
|
+
===== insert
|
249
|
+
|
250
|
+
Allows you to insert an element after an object (default behavior) as a single array. 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 _positional_ arguments for passing as an array to a subsequent step.
|
208
251
|
|
209
|
-
|
252
|
+
Accepts and answers a success. Example:
|
210
253
|
|
211
254
|
[source,ruby]
|
212
255
|
----
|
@@ -216,9 +259,11 @@ pipe %i[a c], insert(:b, at: 1) # Success [:a, :b, :c]
|
|
216
259
|
pipe Failure("Danger!"), insert(:b) # Failure "Danger!"
|
217
260
|
----
|
218
261
|
|
219
|
-
=====
|
262
|
+
===== map
|
263
|
+
|
264
|
+
Allows you to map over an object (enumerable) by wrapping native link:https://rubyapi.org/o/enumerable#method-i-map[Enumerable#map] functionality.
|
220
265
|
|
221
|
-
|
266
|
+
Accepts and answers a success. Example:
|
222
267
|
|
223
268
|
[source,ruby]
|
224
269
|
----
|
@@ -226,9 +271,11 @@ pipe %i[a b c], map(&:inspect) # Success [":a", ":b", ":c"]
|
|
226
271
|
pipe Failure("Danger!"), map(&:inspect) # Failure "Danger!"
|
227
272
|
----
|
228
273
|
|
229
|
-
=====
|
274
|
+
===== merge
|
230
275
|
|
231
|
-
Allows you to merge
|
276
|
+
Allows you to merge an object with additional attributes as a single hash. This step wraps native link:https://rubyapi.org/o/hash#method-i-merge[Hash#merge] functionality. 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 step is most useful when assembling _keyword_ arguments and/or a hash for a subsequent steps.
|
277
|
+
|
278
|
+
Accepts and answers a success. Example:
|
232
279
|
|
233
280
|
[source,ruby]
|
234
281
|
----
|
@@ -238,24 +285,11 @@ pipe "test", merge(as: :a, b: 2) # Success {a: "test", b: 2}
|
|
238
285
|
pipe Failure("Danger!"), merge(b: 2) # Failure "Danger!"
|
239
286
|
----
|
240
287
|
|
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.
|
246
|
-
|
247
|
-
Example:
|
248
|
-
|
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
|
-
----
|
288
|
+
===== tee
|
255
289
|
|
256
|
-
|
290
|
+
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.
|
257
291
|
|
258
|
-
|
292
|
+
Accepts either a success or failure and passes the result through while allowing you to execute arbitrary behavior. Example:
|
259
293
|
|
260
294
|
[source,ruby]
|
261
295
|
----
|
@@ -270,60 +304,83 @@ pipe Failure("Danger!"), tee(Kernel, :puts, "Example.")
|
|
270
304
|
# Failure "Danger!"
|
271
305
|
----
|
272
306
|
|
273
|
-
=====
|
307
|
+
===== to
|
274
308
|
|
275
|
-
Allows you to delegate to an object
|
309
|
+
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`.
|
310
|
+
|
311
|
+
Accepts a success 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`. Example:
|
276
312
|
|
277
313
|
[source,ruby]
|
278
314
|
----
|
279
|
-
Model = Struct.new :label
|
315
|
+
Model = Struct.new :label do
|
280
316
|
include Dry::Monads[:result]
|
281
317
|
|
282
|
-
def self.for(
|
318
|
+
def self.for(**) = Success new(**)
|
283
319
|
end
|
284
320
|
|
285
321
|
pipe({label: "Test"}, to(Model, :for)) # Success #<struct Model label="Test">
|
286
322
|
pipe Failure("Danger!"), to(Model, :for) # Failure "Danger!"
|
287
323
|
----
|
288
324
|
|
289
|
-
=====
|
325
|
+
===== try
|
326
|
+
|
327
|
+
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.
|
290
328
|
|
291
|
-
|
329
|
+
Accepts and answers a success if there are no exceptions. Otherwise, captures any error as a failure. Example:
|
292
330
|
|
293
331
|
[source,ruby]
|
294
332
|
----
|
295
|
-
pipe "test", try(:to_json, catch: JSON::ParserError)
|
296
|
-
|
297
|
-
|
333
|
+
pipe "test", try(:to_json, catch: JSON::ParserError)
|
334
|
+
# Success "\"test\""
|
335
|
+
|
336
|
+
pipe "test", try(:to_json, catch: [JSON::ParserError, StandardError])
|
337
|
+
# Success "\"test\""
|
338
|
+
|
339
|
+
pipe "test", try(:invalid, catch: NoMethodError)
|
340
|
+
# Failure(#<NoMethodError: undefined method `invalid' for an instance of String>)
|
341
|
+
|
342
|
+
pipe Failure("Danger!"), try(:to_json, catch: JSON::ParserError)
|
343
|
+
# Failure "Danger!"
|
298
344
|
----
|
299
345
|
|
300
|
-
=====
|
346
|
+
===== use
|
301
347
|
|
302
|
-
Allows you to use another
|
348
|
+
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).
|
349
|
+
|
350
|
+
Accepts a success 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. Example:
|
303
351
|
|
304
352
|
[source,ruby]
|
305
353
|
----
|
306
|
-
function = ->
|
354
|
+
function = -> number { Success number * 3 }
|
307
355
|
|
308
356
|
pipe 3, use(function) # Success 9
|
309
357
|
pipe Failure("Danger!"), use(function) # Failure "Danger!"
|
310
358
|
----
|
311
359
|
|
312
|
-
=====
|
360
|
+
===== validate
|
361
|
+
|
362
|
+
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`.
|
313
363
|
|
314
|
-
|
364
|
+
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
365
|
|
316
|
-
|
366
|
+
Accepts a success and rewraps as a success if the `:as` keyword is supplied. Otherwise, any failure is immediately passed through. Example:
|
317
367
|
|
318
368
|
[source,ruby]
|
319
369
|
----
|
320
370
|
schema = Dry::Schema.Params { required(:label).filled :string }
|
321
371
|
|
322
|
-
pipe({label: "Test"}, validate(schema))
|
323
|
-
|
324
|
-
|
372
|
+
pipe({label: "Test"}, validate(schema))
|
373
|
+
# Success label: "Test"
|
374
|
+
|
375
|
+
pipe({label: "Test"}, validate(schema, as: nil))
|
376
|
+
# Success #<Dry::Schema::Result{:label=>"Test"} errors={} path=[]>
|
377
|
+
|
378
|
+
pipe Failure("Danger!"), validate(schema)
|
379
|
+
# Failure "Danger!"
|
325
380
|
----
|
326
381
|
|
382
|
+
💡 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.
|
383
|
+
|
327
384
|
==== Advanced
|
328
385
|
|
329
386
|
Several options are available should you need to advance beyond the basic steps. Each is described in detail below.
|
@@ -334,8 +391,8 @@ You can always use a `Proc` as a custom step. Example:
|
|
334
391
|
|
335
392
|
[source,ruby]
|
336
393
|
----
|
337
|
-
include Pipeable
|
338
394
|
include Dry::Monads[:result]
|
395
|
+
include Pipeable
|
339
396
|
|
340
397
|
pipe :a,
|
341
398
|
insert(:b),
|
@@ -355,7 +412,7 @@ include Pipeable
|
|
355
412
|
|
356
413
|
pipe :a,
|
357
414
|
insert(:b),
|
358
|
-
-> result { result.fmap { |
|
415
|
+
-> result { result.fmap { |items| items.join "_" } },
|
359
416
|
as(:to_sym)
|
360
417
|
|
361
418
|
# Yields: Success :a_b
|
@@ -363,35 +420,30 @@ pipe :a,
|
|
363
420
|
|
364
421
|
===== Methods
|
365
422
|
|
366
|
-
Methods
|
423
|
+
Methods, in addition to procs and lambdas, are the _preferred_ way to add custom steps due to the concise syntax. Example:
|
367
424
|
|
368
425
|
[source,ruby]
|
369
426
|
----
|
370
427
|
class Demo
|
371
428
|
include Pipeable
|
372
429
|
|
373
|
-
def call input
|
374
|
-
pipe :a,
|
375
|
-
insert(:b),
|
376
|
-
:join,
|
377
|
-
as(:to_sym)
|
378
|
-
end
|
430
|
+
def call(input) = pipe input, insert(:b), :join, as(:to_sym)
|
379
431
|
|
380
432
|
private
|
381
433
|
|
382
|
-
def join(result) = result.fmap { |
|
434
|
+
def join(result) = result.fmap { |items| items.join "_" }
|
383
435
|
end
|
384
436
|
|
385
|
-
Demo.new.call :a #
|
437
|
+
Demo.new.call :a # Success :a_b
|
386
438
|
----
|
387
439
|
|
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
|
440
|
+
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
441
|
|
390
442
|
===== Custom
|
391
443
|
|
392
444
|
If you'd like to define permanent and reusable steps, you can register a custom step which requires you to:
|
393
445
|
|
394
|
-
. Define a custom step as a
|
446
|
+
. Define a custom step as a class, lambda, or proc.
|
395
447
|
. Register your custom step along side the existing default steps.
|
396
448
|
|
397
449
|
Here's what this would look like:
|
@@ -405,7 +457,7 @@ module CustomSteps
|
|
405
457
|
@delimiter = delimiter
|
406
458
|
end
|
407
459
|
|
408
|
-
def call(result) = result.fmap { |
|
460
|
+
def call(result) = result.fmap { |items| items.join delimiter }
|
409
461
|
|
410
462
|
private
|
411
463
|
|
@@ -418,15 +470,85 @@ Pipeable::Steps::Container.register :join, CustomSteps::Join
|
|
418
470
|
include Pipeable
|
419
471
|
|
420
472
|
pipe :a, insert(:b), join, as(:to_sym)
|
421
|
-
#
|
473
|
+
# Success :a_b
|
422
474
|
|
423
475
|
pipe :a, insert(:b), join(""), as(:to_sym)
|
424
|
-
#
|
476
|
+
# Success :ab
|
477
|
+
----
|
478
|
+
|
479
|
+
A lambda or proc can be used too (albeit in limited capacity). Here's a version of the above using a lambda:
|
480
|
+
|
481
|
+
[source,ruby]
|
482
|
+
----
|
483
|
+
module CustomSteps
|
484
|
+
Join = -> result { result.fmap { |items| items.join "_" } }
|
485
|
+
end
|
486
|
+
|
487
|
+
Pipeable::Steps::Container.register :join, CustomSteps::Join
|
488
|
+
|
489
|
+
include Pipeable
|
490
|
+
|
491
|
+
puts pipe(:a, insert(:b), join, as(:to_sym))
|
492
|
+
# Success :a_b
|
493
|
+
----
|
494
|
+
|
495
|
+
=== Superpipes
|
496
|
+
|
497
|
+
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:
|
498
|
+
|
499
|
+
[source,ruby]
|
425
500
|
----
|
501
|
+
class One
|
502
|
+
include Pipeable
|
503
|
+
|
504
|
+
def initialize label = "one"
|
505
|
+
@label = label
|
506
|
+
end
|
507
|
+
|
508
|
+
def call(item) = pipe item, insert(label, at: 0)
|
509
|
+
|
510
|
+
private
|
511
|
+
|
512
|
+
attr_reader :label
|
513
|
+
end
|
514
|
+
|
515
|
+
class Two
|
516
|
+
include Pipeable
|
517
|
+
|
518
|
+
def initialize label = "two"
|
519
|
+
@label = label
|
520
|
+
end
|
521
|
+
|
522
|
+
def call(item) = pipe item, insert(label)
|
523
|
+
|
524
|
+
private
|
525
|
+
|
526
|
+
attr_reader :label
|
527
|
+
end
|
528
|
+
|
529
|
+
class Three
|
530
|
+
include Pipeable
|
531
|
+
|
532
|
+
def initialize one: One.new, two: Two.new
|
533
|
+
@one = one
|
534
|
+
@two = two
|
535
|
+
end
|
536
|
+
|
537
|
+
def call(item) = pipe item, use(one), use(two)
|
538
|
+
|
539
|
+
private
|
540
|
+
|
541
|
+
attr_reader :one, :two
|
542
|
+
end
|
543
|
+
----
|
544
|
+
|
545
|
+
Notice, `One` and `Two` are normal pipeable objects with individual steps while `Three` injects both `One` and `Two` as dependencies and then subsequently pipes them together in the `#call` method via the `use` step. This is the power of a superpipe. ...and, yes, a superpipe can be an individual step in some other object. Turtles all the way down (or up). 😉
|
546
|
+
|
547
|
+
Again, the above is contrived but hopefully illustrates how you can build more complex architectures from smaller pipes.
|
426
548
|
|
427
549
|
=== Containers
|
428
550
|
|
429
|
-
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
|
551
|
+
Should you not want the basic steps, need custom steps, or a hybrid of default and custom steps, you can define your own container -- using the {containable_link} gem -- and provide the container as an argument to `.[]` when including pipeable behavior. Example:
|
430
552
|
|
431
553
|
[source,ruby]
|
432
554
|
----
|
@@ -448,7 +570,7 @@ pipe :a, echo, insert(:b)
|
|
448
570
|
|
449
571
|
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
572
|
|
451
|
-
Whether you use default, custom, or hybrid steps, you have maximum flexibility using
|
573
|
+
Whether you use default, custom, or hybrid steps, you have maximum flexibility when using containers.
|
452
574
|
|
453
575
|
=== Composition
|
454
576
|
|
@@ -479,18 +601,18 @@ The architecture of this gem is built on top of the following concepts and gems:
|
|
479
601
|
* {function_composition_link}: Made possible through the use of the `\#>>` and `#<<` methods on the link:https://rubyapi.org/3.1/o/method[Method] and link:https://rubyapi.org/3.1/o/proc[Proc] objects.
|
480
602
|
* {containable_link}: Allows related dependencies to be grouped together for injection as desired.
|
481
603
|
* {dry_monads_link}: Critical to ensuring the entire pipeline of steps adhere to the {railway_pattern_link} and leans heavily on the `Result` object.
|
482
|
-
* link:https://alchemists.io/projects/marameters[Marameters]: Through the use of the `.categorize` method, dynamic message passing is possible by inspecting the
|
604
|
+
* link:https://alchemists.io/projects/marameters[Marameters]: Through the use of the `.categorize` method, dynamic message passing is possible by inspecting the object's method parameters.
|
483
605
|
|
484
606
|
=== Style Guide
|
485
607
|
|
486
|
-
* *
|
487
|
-
** Use a single method (i.e. `#call`) which is public and adheres to the {command_pattern_link} so
|
608
|
+
* *Pipes*
|
609
|
+
** 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
610
|
* *Steps*
|
489
611
|
** 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,
|
612
|
+
** 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
613
|
** 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
|
614
|
+
** 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 }`.
|
615
|
+
** 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
616
|
|
495
617
|
=== Debugging
|
496
618
|
|
@@ -500,7 +622,7 @@ If you need to debug (i.e. {debug_link}) your pipe, use a lambda. Example:
|
|
500
622
|
----
|
501
623
|
pipe data,
|
502
624
|
check(/Book.+Price/, :match?),
|
503
|
-
-> result { binding.break },
|
625
|
+
-> result { binding.break; result }, # Breakpoint
|
504
626
|
:parse
|
505
627
|
----
|
506
628
|
|
@@ -508,18 +630,18 @@ The above breakpoint will allow you inspect the result of the `#check` step and/
|
|
508
630
|
|
509
631
|
=== Troubleshooting
|
510
632
|
|
511
|
-
The following might be of aid to as you implement your own
|
633
|
+
The following might be of aid to as you implement your own pipes.
|
512
634
|
|
513
635
|
==== Type Errors
|
514
636
|
|
515
637
|
If you get a `TypeError: Step must be functionally composable and answer a monad`, it means:
|
516
638
|
|
517
|
-
. The step must be a `Proc`, `Method`, or
|
518
|
-
. The step doesn't answer a result monad (i.e. `Success
|
639
|
+
. The step must be a `Proc`, `Method`, or any object which responds to `\#>>`, `#<<`, and `#call`.
|
640
|
+
. The step doesn't answer a result monad (i.e. `Success object` or `Failure object`).
|
519
641
|
|
520
642
|
==== No Method Errors
|
521
643
|
|
522
|
-
If you get a `NoMethodError: undefined method
|
644
|
+
If you get a `NoMethodError: undefined method success?` exception, this might mean that you forgot to add a comma after one of your steps. Example:
|
523
645
|
|
524
646
|
[source,ruby]
|
525
647
|
----
|
@@ -530,7 +652,7 @@ pipe "https://www.wikipedia.org",
|
|
530
652
|
|
531
653
|
# Invalid
|
532
654
|
pipe "https://www.wikipedia.org",
|
533
|
-
to(client, :get)
|
655
|
+
to(client, :get) # Missing comma.
|
534
656
|
try(:parse, catch: HTTP::Error)
|
535
657
|
----
|
536
658
|
|
@@ -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,8 @@ module Pipeable
|
|
8
8
|
module Container
|
9
9
|
extend Containable
|
10
10
|
|
11
|
+
register :alt, Or
|
12
|
+
register :amap, Amap
|
11
13
|
register :as, As
|
12
14
|
register :bind, Bind
|
13
15
|
register :check, Check
|
@@ -15,7 +17,6 @@ module Pipeable
|
|
15
17
|
register :insert, Insert
|
16
18
|
register :map, Map
|
17
19
|
register :merge, Merge
|
18
|
-
register :orr, Or
|
19
20
|
register :tee, Tee
|
20
21
|
register :to, To
|
21
22
|
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.6.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-12 00:00:00.000000000 Z
|
39
39
|
dependencies:
|
40
40
|
- !ruby/object:Gem::Dependency
|
41
41
|
name: containable
|
@@ -123,6 +123,7 @@ files:
|
|
123
123
|
- lib/pipeable/composable.rb
|
124
124
|
- lib/pipeable/pipe.rb
|
125
125
|
- lib/pipeable/steps/abstract.rb
|
126
|
+
- lib/pipeable/steps/amap.rb
|
126
127
|
- lib/pipeable/steps/as.rb
|
127
128
|
- lib/pipeable/steps/bind.rb
|
128
129
|
- lib/pipeable/steps/check.rb
|
@@ -164,7 +165,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
164
165
|
- !ruby/object:Gem::Version
|
165
166
|
version: '0'
|
166
167
|
requirements: []
|
167
|
-
rubygems_version: 3.5.
|
168
|
+
rubygems_version: 3.5.10
|
168
169
|
signing_key:
|
169
170
|
specification_version: 4
|
170
171
|
summary: A domain specific language for building functionally composable steps.
|
metadata.gz.sig
CHANGED
Binary file
|