pipeable 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2467c8bb8dda8efd0a2e4fc5a78765f8abaeab65c288c380d9c7358244ba5aaf
4
- data.tar.gz: 96c96e5e12d25f81dd9c7e83b59850691036513b8456acab4868b93a71083f22
3
+ metadata.gz: 4c03f09d3e8268f1786faa5907fe4f9dbc4beaf02c874bec73b683fd1e4a51de
4
+ data.tar.gz: 29943bc0fed70a2661cee558e5a4d23090af2fe216ba085ec1ff00e3da435ea3
5
5
  SHA512:
6
- metadata.gz: 94f1002860d093423864900306a266a8c5d452f186f40e6053c326fc9a689cf0b7edded0a60f92d00dd1de14f906315f6b88a7987fb0b67511f9a92fdd73f160
7
- data.tar.gz: 767a44496263b160871c38a7bef274eacc089dbf641fdb60a7879f07e584603c0fc125cf9b285c156a75daff4e6cc856c5b027307f87a8f1f34c0876dcba33ce
6
+ metadata.gz: ebaeb186fb3c672501fa3b5414b2d4fe381a45fa22004ae2f128b80abc15d012f0ff70befb900ad75d82b760af33fa4a35c7db5d3113e7f3f261782f01e7dd24
7
+ data.tar.gz: 41fe4361d2a0226ceeb8796d1edc9e6fd0e19bc00539c3d3c4e87c57d09a871818d051d7a8d881b294c0f3ea8e73f2575a0ead5dcaaf3d44dbbb0dd53065d749
checksums.yaml.gz.sig CHANGED
Binary file
data/README.adoc CHANGED
@@ -21,7 +21,7 @@ toc::[]
21
21
 
22
22
  == Features
23
23
 
24
- * Built atop of native {function_composition_link}.
24
+ * Built atop native {function_composition_link}.
25
25
  * Adheres to the {railway_pattern_link}.
26
26
  * Provides built-in and customizable domain-specific steps.
27
27
  * Provides chainable _pipes_ which can be used to build more complex workflows.
@@ -131,7 +131,7 @@ pipe(input, *steps)
131
131
 
132
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}.
133
133
 
134
- Behind the scenes, the `#pipe` method is syntactic sugar on top of {function_composition_link} which means if this code were to be rewritten:
134
+ Behind the scenes, the `#pipe` method is syntactic sugar built atop {function_composition_link} which means if this code were to be rewritten:
135
135
 
136
136
  [source,ruby]
137
137
  ----
@@ -141,7 +141,7 @@ pipe csv,
141
141
  map { |item| "#{item[:book]}: #{item[:price]}" }
142
142
  ----
143
143
 
144
- Then the above would look like this using native Ruby:
144
+ ...then the above would look like the following (as rewritten in native Ruby):
145
145
 
146
146
  [source,ruby]
147
147
  ----
@@ -152,27 +152,28 @@ Then the above would look like this using native Ruby:
152
152
  ).call Success(csv)
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
+
155
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.
156
160
 
157
161
  === Steps
158
162
 
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).
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:
160
164
 
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.
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).
162
167
 
163
168
  ==== Basic
164
169
 
165
170
  The following are the basic (default) steps for building custom pipes for which you can mix and match within your own implementation.
166
171
 
167
- ===== Alt
172
+ ===== alt
168
173
 
169
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.
170
175
 
171
- *I/O*
172
-
173
- Processes a failure only while expecting you to answer a success or failure.
174
-
175
- *Example*
176
+ Accepts a failure while answering either a success or failure. Example:
176
177
 
177
178
  [source,ruby]
178
179
  ----
@@ -181,15 +182,23 @@ pipe Failure("Danger!"), alt { Success "Resolved" } # Success "Re
181
182
  pipe Failure("Danger!"), alt { |object| Failure "Big #{object}" } # Failure "Big Danger!"
182
183
  ----
183
184
 
184
- ===== As
185
+ ===== amap
185
186
 
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
+ 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.
187
188
 
188
- *I/O*
189
+ Accepts and answers a failure. Example:
189
190
 
190
- Processes and answers a success only.
191
+ [source,ruby]
192
+ ----
193
+ pipe Failure("Danger"), amap { |object| "#{object}!" } # Failure "Danger!"
194
+ pipe Success("Pass"), amap { |object| "#{object}!" } # Success "Pass"
195
+ ----
191
196
 
192
- *Example*
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:
193
202
 
194
203
  [source,ruby]
195
204
  ----
@@ -198,15 +207,11 @@ pipe %i[a b c], as(:dig, 1) # Success :b
198
207
  pipe Failure("Danger!"), as(:inspect) # Failure "Danger!"
199
208
  ----
200
209
 
201
- ===== Bind
210
+ ===== bind
202
211
 
203
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.
204
213
 
205
- *I/O*
206
-
207
- Processes a success only while expecting you to answer a success or failure in return.
208
-
209
- *Example*
214
+ Accepts a success while answering either a success or failure. Example:
210
215
 
211
216
  [source,ruby]
212
217
  ----
@@ -215,15 +220,11 @@ pipe %i[a b c], bind { |object| Failure object } # Failure [
215
220
  pipe Failure("Danger!"), bind { |object| Success object.join("-") } # Failure "Danger!"
216
221
  ----
217
222
 
218
- ===== Check
223
+ ===== check
219
224
 
220
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`.
221
226
 
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*
227
+ Accepts a success while answering a success or failure depending on whether unwrapped object checks against the proof. Example:
227
228
 
228
229
  [source,ruby]
229
230
  ----
@@ -232,15 +233,11 @@ pipe :a, check(%i[b c], :include?) # Failure :a
232
233
  pipe Failure("Danger!"), check(%i[a b], :include?) # Failure "Danger!"
233
234
  ----
234
235
 
235
- ===== Fmap
236
+ ===== fmap
236
237
 
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
+ 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.
238
239
 
239
- *I/O*
240
-
241
- Processes and answers a success only.
242
-
243
- *Example*
240
+ Accepts and answers a success. Example:
244
241
 
245
242
  [source,ruby]
246
243
  ----
@@ -248,15 +245,11 @@ pipe %i[a b c], fmap { |object| object.join "-" } # Success "a-b-c"
248
245
  pipe Failure("Danger!"), fmap { |object| object.join "-" } # Failure "Danger!"
249
246
  ----
250
247
 
251
- ===== Insert
252
-
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*
248
+ ===== insert
256
249
 
257
- Processes and answers a success only.
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.
258
251
 
259
- *Example*
252
+ Accepts and answers a success. Example:
260
253
 
261
254
  [source,ruby]
262
255
  ----
@@ -266,15 +259,11 @@ pipe %i[a c], insert(:b, at: 1) # Success [:a, :b, :c]
266
259
  pipe Failure("Danger!"), insert(:b) # Failure "Danger!"
267
260
  ----
268
261
 
269
- ===== Map
262
+ ===== map
270
263
 
271
264
  Allows you to map over an object (enumerable) by wrapping native link:https://rubyapi.org/o/enumerable#method-i-map[Enumerable#map] functionality.
272
265
 
273
- *I/O*
274
-
275
- Processes and answers a success only.
276
-
277
- *Example*
266
+ Accepts and answers a success. Example:
278
267
 
279
268
  [source,ruby]
280
269
  ----
@@ -282,15 +271,11 @@ pipe %i[a b c], map(&:inspect) # Success [":a", ":b", ":c"]
282
271
  pipe Failure("Danger!"), map(&:inspect) # Failure "Danger!"
283
272
  ----
284
273
 
285
- ===== Merge
286
-
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
274
+ ===== merge
288
275
 
289
- *I/O*
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.
290
277
 
291
- Processes and answers a success only.
292
-
293
- *Example*
278
+ Accepts and answers a success. Example:
294
279
 
295
280
  [source,ruby]
296
281
  ----
@@ -300,15 +285,11 @@ pipe "test", merge(as: :a, b: 2) # Success {a: "test", b: 2}
300
285
  pipe Failure("Danger!"), merge(b: 2) # Failure "Danger!"
301
286
  ----
302
287
 
303
- ===== Tee
288
+ ===== tee
304
289
 
305
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.
306
291
 
307
- *I/O*
308
-
309
- Passes the result through while allowing you to execute arbitrary behavior.
310
-
311
- *Example*
292
+ Accepts either a success or failure and passes the result through while allowing you to execute arbitrary behavior. Example:
312
293
 
313
294
  [source,ruby]
314
295
  ----
@@ -323,15 +304,11 @@ pipe Failure("Danger!"), tee(Kernel, :puts, "Example.")
323
304
  # Failure "Danger!"
324
305
  ----
325
306
 
326
- ===== To
307
+ ===== to
327
308
 
328
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`.
329
310
 
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*
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:
335
312
 
336
313
  [source,ruby]
337
314
  ----
@@ -345,15 +322,11 @@ pipe({label: "Test"}, to(Model, :for)) # Success #<struct Model label="Test">
345
322
  pipe Failure("Danger!"), to(Model, :for) # Failure "Danger!"
346
323
  ----
347
324
 
348
- ===== Try
325
+ ===== try
349
326
 
350
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.
351
328
 
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*
329
+ Accepts and answers a success if there are no exceptions. Otherwise, captures any error as a failure. Example:
357
330
 
358
331
  [source,ruby]
359
332
  ----
@@ -370,15 +343,11 @@ pipe Failure("Danger!"), try(:to_json, catch: JSON::ParserError)
370
343
  # Failure "Danger!"
371
344
  ----
372
345
 
373
- ===== Use
346
+ ===== use
374
347
 
375
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).
376
349
 
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*
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:
382
351
 
383
352
  [source,ruby]
384
353
  ----
@@ -388,19 +357,13 @@ pipe 3, use(function) # Success 9
388
357
  pipe Failure("Danger!"), use(function) # Failure "Danger!"
389
358
  ----
390
359
 
391
- ===== Validate
360
+ ===== validate
392
361
 
393
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`.
394
363
 
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.
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.
398
365
 
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*
366
+ Accepts a success and rewraps as a success if the `:as` keyword is supplied. Otherwise, any failure is immediately passed through. Example:
404
367
 
405
368
  [source,ruby]
406
369
  ----
@@ -416,6 +379,8 @@ pipe Failure("Danger!"), validate(schema)
416
379
  # Failure "Danger!"
417
380
  ----
418
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
+
419
384
  ==== Advanced
420
385
 
421
386
  Several options are available should you need to advance beyond the basic steps. Each is described in detail below.
@@ -577,13 +542,13 @@ class Three
577
542
  end
578
543
  ----
579
544
 
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). 😉
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). 😉
581
546
 
582
- Again, the above is contrived but hopefully this illustrates how you can build more complex architectures from smaller pipes.
547
+ Again, the above is contrived but hopefully illustrates how you can build more complex architectures from smaller pipes.
583
548
 
584
549
  === Containers
585
550
 
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:
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:
587
552
 
588
553
  [source,ruby]
589
554
  ----
@@ -636,7 +601,7 @@ The architecture of this gem is built on top of the following concepts and gems:
636
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.
637
602
  * {containable_link}: Allows related dependencies to be grouped together for injection as desired.
638
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.
639
- * link:https://alchemists.io/projects/marameters[Marameters]: Through the use of the `.categorize` method, dynamic message passing is possible by inspecting the operation method's parameters.
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.
640
605
 
641
606
  === Style Guide
642
607
 
@@ -657,7 +622,7 @@ If you need to debug (i.e. {debug_link}) your pipe, use a lambda. Example:
657
622
  ----
658
623
  pipe data,
659
624
  check(/Book.+Price/, :match?),
660
- -> result { binding.break }, # Breakpoint
625
+ -> result { binding.break; result }, # Breakpoint
661
626
  :parse
662
627
  ----
663
628
 
@@ -671,12 +636,12 @@ The following might be of aid to as you implement your own pipes.
671
636
 
672
637
  If you get a `TypeError: Step must be functionally composable and answer a monad`, it means:
673
638
 
674
- . The step must be a `Proc`, `Method`, or some object which responds to `\#>>`, `#<<`, and `#call`.
675
- . The step doesn't answer a result monad (i.e. `Success some_value` or `Failure some_value`).
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`).
676
641
 
677
642
  ==== No Method Errors
678
643
 
679
- 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:
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:
680
645
 
681
646
  [source,ruby]
682
647
  ----
@@ -687,7 +652,7 @@ pipe "https://www.wikipedia.org",
687
652
 
688
653
  # Invalid
689
654
  pipe "https://www.wikipedia.org",
690
- to(client, :get) # Missing comma.
655
+ to(client, :get) # Missing comma.
691
656
  try(:parse, catch: HTTP::Error)
692
657
  ----
693
658
 
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pipeable
4
+ module Steps
5
+ # Wraps Dry Monads `#alt_map` method as a step.
6
+ class Amap < Abstract
7
+ def call(result) = result.alt_map { |object| base_block.call object }
8
+ end
9
+ end
10
+ end
@@ -9,6 +9,7 @@ module Pipeable
9
9
  extend Containable
10
10
 
11
11
  register :alt, Or
12
+ register :amap, Amap
12
13
  register :as, As
13
14
  register :bind, Bind
14
15
  register :check, Check
data/pipeable.gemspec CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |spec|
4
4
  spec.name = "pipeable"
5
- spec.version = "0.5.0"
5
+ spec.version = "0.6.0"
6
6
  spec.authors = ["Brooke Kuhlmann"]
7
7
  spec.email = ["brooke@alchemists.io"]
8
8
  spec.homepage = "https://alchemists.io/projects/pipeable"
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pipeable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.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-05-07 00:00:00.000000000 Z
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
metadata.gz.sig CHANGED
Binary file