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