pipeable 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3e0aa08638b53d5dc7005d8754db2e991a9529d439ebedef96baa7fb4ef0e67b
4
- data.tar.gz: 16cab1671faa230b629f19c68d2b16835e3b2e9406085b8e08ae1281194c2273
3
+ metadata.gz: 4c03f09d3e8268f1786faa5907fe4f9dbc4beaf02c874bec73b683fd1e4a51de
4
+ data.tar.gz: 29943bc0fed70a2661cee558e5a4d23090af2fe216ba085ec1ff00e3da435ea3
5
5
  SHA512:
6
- metadata.gz: b4a029e0fe184140e2ccb0649753d19c2250e5231df82ef2fd8961e74e622330090891288db8654e5d0282ff41b459587901b14dba37426407d6fd31ba6b53cc
7
- data.tar.gz: 31a1d08c5c4f068fdf04bfcc92b21308ff1eee4e551da94db16557d7b6c2b3621c90728519addf1ac0e101134b2056450017f1e0f7cab83ee098dcc0cb2e5174
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 left-to-right or top-to-bottom which results in a success or a failure without having to rely on exceptions which are expensive and should not be used for control flow.
18
+ A DSL for workflows built atop native {function_composition_link} which leverages the {railway_pattern_link}. This allows you to write a sequence of _steps_ that cleanly read from top-to-bottom or left-to-right resulting in a single success or a failure. This allows you to avoid relying on exceptions for expensive control flows and/or complex conditional logic in general.
18
19
 
19
20
  toc::[]
20
21
 
21
22
  == Features
22
23
 
23
- * Built atop of native {function_composition_link}.
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 _transaction_ by requiring and including this gem as follows:
69
+ You can turn any object into a _pipe_ by requiring and including this gem as follows:
69
70
 
70
71
  [source,ruby]
71
72
  ----
@@ -100,7 +101,7 @@ class Demo
100
101
  end
101
102
  ----
102
103
 
103
- The above allows `Demo#call` to be a sequence steps which may pass or fail due to all step being {dry_monads_link}. This is the essence of the {railway_pattern_link}.
104
+ The above allows `Demo#call` to be a sequence of steps which may pass or fail due to all steps using {dry_monads_link} for input and output. This is the essence of the {railway_pattern_link}.
104
105
 
105
106
  To execute the above example, you'd only need to pass CSV content to it:
106
107
 
@@ -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 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:
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
- Then the above would look like this using native Ruby:
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
- The problem with native function composition is that it reads backwards by passing your input at the end of all sequential steps. With the `#pipe` method, you have the benefit of allowing your eye to read the code from top to bottom in addition to not having to type multiple _forward composition_ operators.
155
+ 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 attempt to turn the failure into a success). The following sections detail how to mix and match steps for building a robust implementation.
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 more advanced functionality.
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
- ===== As
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
- Allows you to message the input as different output. Example:
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
- ===== Bind
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
- Allows you to perform operations on a successful result only. You are then responsible for answering a success or failure accordingly. This is a convenience wrapper to native {dry_monads_link} `#bind` functionality. Example:
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 { |input| Success input.join("-") } # Success "a-b-c"
182
- pipe %i[a b c], bind { |input| Failure input } # Failure [:a, :b, :c]
183
- pipe Failure("Danger!"), bind { |input| Success input.join("-") } # Failure "Danger!"
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
- ===== Check
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
- Allows you to check if the input and messaged object evaluate to `true` or `Success`. When successful, input is passed through as a `Success`. When false, input is passed through as a `Failure`. Example:
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
- ===== Fmap
236
+ ===== fmap
198
237
 
199
- Allows you to unwrap a successful operation, make a modification, and rewrap the modification as a new success. This is a convenience wrapper to native {dry_monads_link} `#fmap` functionality. Example:
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 { |input| input.join "-" } # Success "a-b-c"
204
- pipe Failure("Danger!"), fmap { |input| input.join "-" } # Failure "Danger!"
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
- ===== Insert
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
- Allows you to insert an element after the input (default behavior) and wraps native link:https://rubyapi.org/o/array#method-i-insert[Array#insert] functionality. If the input is not an array, it will be cast as one. You can use the `:at` key to specify where you want insertion to happen. This step is most useful when needing to assemble arguments for passing to a subsequent step. Example:
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
- ===== Map
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
- Allows you to map over an enumerable and wraps native link:https://rubyapi.org/o/enumerable#method-i-map[Enumerable#map] functionality.
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
- ===== Merge
274
+ ===== merge
230
275
 
231
- Allows you to merge the input with additional attributes as a single hash. If the input is not a hash, then the input will be merged with the attributes using `step` as the key. The default `step` key can be renamed to a different key by using the `:as` key. Like the _Insert_ step, this is most useful when needing to assemble arguments and/or data for consumption by subsequent steps. Example:
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
- ===== Orr
242
-
243
- Allows you to operate on a failure and produce either a success or another failure. This is a convenience wrapper to native {dry_monads_link} `#or` functionality.
244
-
245
- ℹ️ Syntactically, `or` can't be used for this step since `or` is a native Ruby keyword so `orr` is used instead.
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
- ===== Tee
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
- Allows you to run an operation and ignore the response while input is passed through as output. This behavior is similar in nature to the link:https://www.gnu.org/savannah-checkouts/gnu/gawk/manual/html_node/Tee-Program.html[tee] program in Bash. Example:
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
- ===== To
307
+ ===== to
274
308
 
275
- Allows you to delegate to an object -- which doesn't have a callable interface and may or may not answer a result -- for processing of input. If the response is not a monad, it'll be automatically wrapped as a `Success`. Example:
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, keyword_init: true do
315
+ Model = Struct.new :label do
280
316
  include Dry::Monads[:result]
281
317
 
282
- def self.for(...) = Success new(...)
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
- ===== Try
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
- Allows you to try an operation which may fail while catching the exception as a failure for further processing. Example:
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) # Success "\"test\""
296
- pipe "test", try(:invalid, catch: NoMethodError) # Failure "undefined method..."
297
- pipe Failure("Danger!"), try(:to_json, catch: JSON::ParserError) # Failure "Danger!"
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
- ===== Use
346
+ ===== use
301
347
 
302
- Allows you to use another transaction which might have multiple steps of it's own, use an object that adheres to the {command_pattern_link}, or any function which answers a {dry_monads_link} `Result` object. In other words, you can use _use_ any object which responds to `#call` and answers a {dry_monads_link} `Result` object. This is great for chaining multiple transactions together.
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 = -> input { Success input * 3 }
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
- ===== Validate
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
- Allows you to use an operation that will validate the input. This is especially useful when using {dry_schema_link}, {dry_validation_link}, or any operation that can respond to `#call` while answering a result that can be converted into a hash.
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
- By default, the `:as` key uses `:to_h` as it's value so you get automatic casting to a `Hash`. Use `nil`, as the value, to disable this behavior. You can also pass in any value to the `:as` key which is a valid method that the result will respond to.
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)) # Success label: "Test"
323
- pipe({label: "Test"}, validate(schema, as: nil)) # Success #<Dry::Schema::Result{:label=>"Test"} errors={} path=[]>
324
- pipe Failure("Danger!"), validate(schema) # Failure "Danger!"
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 { |input| input.join "_" } },
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 -- in addition to procs and lambdas -- are the _preferred_ way to add custom steps due to the concise syntax. Example:
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 { |input| input.join "_" }
434
+ def join(result) = result.fmap { |items| items.join "_" }
383
435
  end
384
436
 
385
- Demo.new.call :a # Yields: Success :a_b
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 the use of the `:join` symbol is the same as using `method(:join)`. Both work but the former requires less typing than the latter.
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 new class.
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 { |input| input.join delimiter }
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
- # Yields: Success :a_b
473
+ # Success :a_b
422
474
 
423
475
  pipe :a, insert(:b), join(""), as(:to_sym)
424
- # Yields: Success :ab
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 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:
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 this approach.
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 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.
483
605
 
484
606
  === Style Guide
485
607
 
486
- * *Transactions*
487
- ** Use a single method (i.e. `#call`) which is public and adheres to the {command_pattern_link} so transactions can be piped together if desired.
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, the unused arguments -- need to be passed up to the superclass from the subclass (i.e. `super(*positionals, **keywords, &block)`). Doing so allows the superclass (i.e. `Abstract`) to provide access to `base_positionals`, `base_keywords`, and `base_block` for use if desired by the subclass.
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 `input` parameter to be consistent. More specific parameters like `argument` or `operation` should be used to improve readability when possible. Example: `def call(result) = result.bind { |input| # Implementation }`.
493
- ** Use implicit blocks sparingly. Most of the default steps shy away from using blocks because it can make the code more complex. Use private methods, custom steps, and/or separate transactions if the code becomes too complex because you might have a smaller object which needs extraction.
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 }, # Breakpoint
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 transactions.
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 some object which responds to `\#>>`, `#<<`, and `#call`.
518
- . 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`).
519
641
 
520
642
  ==== No Method Errors
521
643
 
522
- 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:
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) # Missing comma.
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, :marameters
20
+ attr_reader :base_positionals, :base_keywords, :base_block
23
21
  end
24
22
  end
25
23
  end
@@ -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
@@ -2,10 +2,10 @@
2
2
 
3
3
  module Pipeable
4
4
  module Steps
5
- # Allows result to be messaged as a callable.
5
+ # Messages object, with optional arguments, as different result.
6
6
  class As < Abstract
7
7
  def call result
8
- result.fmap { |operation| operation.public_send(*base_positionals, **base_keywords) }
8
+ result.fmap { |object| object.public_send(*base_positionals, **base_keywords) }
9
9
  end
10
10
  end
11
11
  end
@@ -4,7 +4,7 @@ module Pipeable
4
4
  module Steps
5
5
  # Wraps Dry Monads `#bind` method as a step.
6
6
  class Bind < Abstract
7
- def call(result) = result.bind { |input| base_block.call input }
7
+ def call(result) = result.bind { |object| base_block.call object }
8
8
  end
9
9
  end
10
10
  end
@@ -1,29 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "marameters"
4
+
3
5
  module Pipeable
4
6
  module Steps
5
- # Checks if operation is true and then answers success (passthrough) or failure (with argument).
7
+ # Checks if proof is true and answers success (passthrough) or failure (with optional argument).
6
8
  class Check < Abstract
7
- def initialize(operation, message, **)
8
- super(**)
9
- @operation = operation
9
+ def initialize proof, message
10
+ super()
11
+ @proof = proof
10
12
  @message = message
11
13
  end
12
14
 
13
15
  def call result
14
- result.bind do |arguments|
15
- answer = question arguments
16
- answer == true || answer.is_a?(Success) ? result : Failure(arguments)
16
+ result.bind do |object|
17
+ answer = question object
18
+ answer == true || answer.is_a?(Success) ? result : Failure(object)
17
19
  end
18
20
  end
19
21
 
20
22
  private
21
23
 
22
- attr_reader :operation, :message
24
+ attr_reader :proof, :message
23
25
 
24
- def question arguments
25
- splat = marameters.categorize operation.method(message).parameters, arguments
26
- operation.public_send(message, *splat.positionals, **splat.keywords, &splat.block)
26
+ def question object
27
+ splat = Marameters.categorize proof.method(message).parameters, object
28
+ proof.public_send(message, *splat.positionals, **splat.keywords, &splat.block)
27
29
  end
28
30
  end
29
31
  end
@@ -8,6 +8,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
@@ -4,7 +4,7 @@ module Pipeable
4
4
  module Steps
5
5
  # Wraps Dry Monads `#fmap` method as a step.
6
6
  class Fmap < Abstract
7
- def call(result) = result.fmap { |input| base_block.call input }
7
+ def call(result) = result.fmap { |object| base_block.call object }
8
8
  end
9
9
  end
10
10
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Pipeable
4
4
  module Steps
5
- # Inserts elements before, after, or around input.
5
+ # Inserts elements before or after an object.
6
6
  class Insert < Abstract
7
7
  LAST = -1
8
8
 
@@ -13,8 +13,8 @@ module Pipeable
13
13
  end
14
14
 
15
15
  def call result
16
- result.fmap do |input|
17
- cast = input.is_a?(Array) ? input : [input]
16
+ result.fmap do |object|
17
+ cast = object.is_a?(Array) ? object : [object]
18
18
  value.is_a?(Array) ? cast.insert(at, *value) : cast.insert(at, value)
19
19
  end
20
20
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Pipeable
4
4
  module Steps
5
- # Maps over a collection, processing each element, and answering a new result.
5
+ # Maps over an enumerable, processes each element, and answers a new enumerable.
6
6
  class Map < Abstract
7
7
  def call(result) = result.fmap { |collection| collection.map(&base_block) }
8
8
  end
@@ -2,19 +2,19 @@
2
2
 
3
3
  module Pipeable
4
4
  module Steps
5
- # Merges initialized attributes with step argument for use by a subsequent step.
5
+ # Merges initialized attributes with step object for use by subsequent step.
6
6
  class Merge < Abstract
7
- def initialize as: :step, **keywords
8
- super(**keywords)
7
+ def initialize(as: :step, **)
8
+ super(**)
9
9
  @as = as
10
10
  end
11
11
 
12
12
  def call result
13
- result.fmap do |input|
14
- if input.is_a? Hash
15
- input.merge! base_keywords
13
+ result.fmap do |object|
14
+ if object.is_a? Hash
15
+ object.merge! base_keywords
16
16
  else
17
- {as => input}.merge!(base_keywords)
17
+ {as => object}.merge!(base_keywords)
18
18
  end
19
19
  end
20
20
  end
@@ -4,7 +4,7 @@ module Pipeable
4
4
  module Steps
5
5
  # Wraps Dry Monads `#or` method as a step.
6
6
  class Or < Abstract
7
- def call(result) = result.or { |input| base_block.call input }
7
+ def call(result) = result.or { |object| base_block.call object }
8
8
  end
9
9
  end
10
10
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Pipeable
4
4
  module Steps
5
- # Messages operation, without any response checks, while passing input through as output.
5
+ # Messages operation, without any checks, while passing input through as output.
6
6
  class Tee < Abstract
7
7
  def initialize(operation, *, **)
8
8
  super(*, **)
@@ -1,25 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "marameters"
4
+
3
5
  module Pipeable
4
6
  module Steps
5
- # Delegates to a non-callable operation which automatically wraps the result if necessary.
7
+ # Delegates to a non-callable object which automatically wraps the result if necessary.
6
8
  class To < Abstract
7
- def initialize(operation, message, **)
9
+ def initialize(object, message, **)
8
10
  super(**)
9
- @operation = operation
11
+ @object = object
10
12
  @message = message
11
13
  end
12
14
 
13
15
  def call result
14
16
  result.bind do |arguments|
15
- splat = marameters.categorize operation.method(message).parameters, arguments
16
- wrap operation.public_send(message, *splat.positionals, **splat.keywords, &splat.block)
17
+ splat = Marameters.categorize object.method(message).parameters, arguments
18
+ wrap object.public_send(message, *splat.positionals, **splat.keywords, &splat.block)
17
19
  end
18
20
  end
19
21
 
20
22
  private
21
23
 
22
- attr_reader :operation, :message
24
+ attr_reader :object, :message
23
25
 
24
26
  def wrap(result) = result.is_a?(Dry::Monads::Result) ? result : Success(result)
25
27
  end
@@ -2,17 +2,17 @@
2
2
 
3
3
  module Pipeable
4
4
  module Steps
5
- # Messages a risky operation which may pass or fail.
5
+ # Sends a risky message to an object which may pass or fail.
6
6
  class Try < Abstract
7
- def initialize *positionals, catch:, **keywords
8
- super(*positionals, **keywords)
7
+ def initialize(*, catch:, **)
8
+ super(*, **)
9
9
  @catch = catch
10
10
  end
11
11
 
12
12
  def call result
13
- result.fmap { |operation| operation.public_send(*base_positionals, **base_keywords) }
13
+ result.fmap { |object| object.public_send(*base_positionals, **base_keywords) }
14
14
  rescue *Array(catch) => error
15
- Failure error.message
15
+ Failure error
16
16
  end
17
17
 
18
18
  private
@@ -2,18 +2,18 @@
2
2
 
3
3
  module Pipeable
4
4
  module Steps
5
- # Use another transaction -- or any command -- which answers a result.
5
+ # Messages a command (or pipe) which answers a result.
6
6
  class Use < Abstract
7
- def initialize(operation, **)
7
+ def initialize(command, **)
8
8
  super(**)
9
- @operation = operation
9
+ @command = command
10
10
  end
11
11
 
12
- def call(result) = result.bind { |input| operation.call input }
12
+ def call(result) = result.bind { |input| command.call input }
13
13
 
14
14
  private
15
15
 
16
- attr_reader :operation
16
+ attr_reader :command
17
17
  end
18
18
  end
19
19
  end
@@ -2,27 +2,23 @@
2
2
 
3
3
  module Pipeable
4
4
  module Steps
5
- # Validates a result via a callable operation.
5
+ # Validates result via a callable contract.
6
6
  class Validate < Abstract
7
- def initialize(operation, as: :to_h, **)
8
- super(**)
9
- @operation = operation
7
+ def initialize contract, as: nil
8
+ super()
9
+ @contract = contract
10
10
  @as = as
11
11
  end
12
12
 
13
- def call result
14
- result.bind do |payload|
15
- value = operation.call payload
16
-
17
- return Failure value if value.failure?
18
-
19
- Success(as ? value.public_send(as) : value)
20
- end
21
- end
13
+ def call(result) = result.bind { |payload| cast payload }
22
14
 
23
15
  private
24
16
 
25
- attr_reader :operation, :as
17
+ attr_reader :contract, :as
18
+
19
+ def cast payload
20
+ contract.call(payload).to_monad.fmap { |data| as ? data.public_send(as) : data }
21
+ end
26
22
  end
27
23
  end
28
24
  end
data/pipeable.gemspec CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |spec|
4
4
  spec.name = "pipeable"
5
- spec.version = "0.4.0"
5
+ spec.version = "0.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.4.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-04-30 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
@@ -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.9
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