transactable 0.0.0 → 0.1.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: 1273b58f01b4687af4c24782ecd585c4c57ce7bbb434195e8390d84c7561ad1b
4
- data.tar.gz: 2b60852cb72d7039c12f27314dae85950c78a896339a2cd15372df7476f8dbc5
3
+ metadata.gz: c58179b7d8fa50cf463e7cbbf0fbaf83382d10478df233121400f110fcfee5dd
4
+ data.tar.gz: 417af3965b8ab1fb1421b619aa117abc129e173cea03e5931204e277250086c5
5
5
  SHA512:
6
- metadata.gz: 7c388a05f4f11b7cd5106679ddb77a0fcd60de068f528f4922cb27b85286e3e0d04aa4b457e4bacc423f178542876896c722b0bf203a0621565bfc199db7828a
7
- data.tar.gz: c91bf7b5d36bea0ebf70481d1c62e45db30be3afc5f5dc5425ab2eda9be65cafc5d032efad630e942a4902b135d92c491543e798058119b9808b150c56f8d933
6
+ metadata.gz: '029ca350dbfd66cf6e076887456434903c76c302b4906ea6f6c93371d2be0ed5c51b5035445e3519c7c6f72114c62f6285704c5ff520eb6af822d6f27ffc484c'
7
+ data.tar.gz: 6801347ce93c433a9a8a3fa28b03ce1ac47b9ee8c68bb5b37cab1b3fd222abd3d461af7164de2e99f85000ae977401c00943c094787f10c6a77871256a2f73a6
checksums.yaml.gz.sig CHANGED
Binary file
data/README.adoc CHANGED
@@ -1,14 +1,16 @@
1
+ :toc: macro
2
+ :toclevels: 5
3
+ :figure-caption!:
4
+
1
5
  :command_pattern_link: link:https://www.alchemists.io/articles/command_pattern[Command Pattern]
6
+ :function_composition_link: link:https://www.alchemists.io/articles/ruby_function_composition[Function Composition]
7
+ :debug_link: link:https://github.com/ruby/debug[Debug]
2
8
  :dry_container_link: link:https://dry-rb.org/gems/dry-container[Dry Container]
3
9
  :dry_events_link: link:https://dry-rb.org/gems/dry-events[Dry Events]
4
10
  :dry_monads_link: link:https://dry-rb.org/gems/dry-monads[Dry Monads]
5
11
  :dry_schema_link: link:https://dry-rb.org/gems/dry-schema[Dry Schema]
6
12
  :dry_validation_link: link:https://dry-rb.org/gems/dry-validation[Dry Validation]
7
13
 
8
- :toc: macro
9
- :toclevels: 5
10
- :figure-caption!:
11
-
12
14
  = Transactable
13
15
 
14
16
  A DSL for transactional workflows built atop function composition. This allows you to write in a syntax that builds upon -- and abstracts away -- native function composition support while allowing you to cleanly read the code from left-to-right or top-to-bottom sequentially.
@@ -25,6 +27,7 @@ toc::[]
25
27
  == Requirements
26
28
 
27
29
  . link:https://www.ruby-lang.org[Ruby].
30
+ . A strong understanding of {function_composition_link}.
28
31
 
29
32
  == Setup
30
33
 
@@ -138,9 +141,13 @@ The only problem with native function composition is that it reads backwards by
138
141
 
139
142
  === Steps
140
143
 
141
- There are multiple, default, steps you can use to compose your transactional 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 describes each step in detail where you can mix and match as makes sense.
144
+ There are several ways to compose steps for your transactional 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.
145
+
146
+ ==== Basic
142
147
 
143
- ==== As
148
+ The following are the basic (default) steps for building for more advanced functionality.
149
+
150
+ ===== As
144
151
 
145
152
  Allows you to message the input as different output. Example:
146
153
 
@@ -151,7 +158,7 @@ pipe %i[a b c], as(:dig, 1) # Success :b
151
158
  pipe Failure("Danger!"), as(:inspect) # Failure "Danger!"
152
159
  ----
153
160
 
154
- ==== Bind
161
+ ===== Bind
155
162
 
156
163
  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:
157
164
 
@@ -162,7 +169,7 @@ pipe %i[a b c], bind { |input| Failure input } # Failure [:a
162
169
  pipe Failure("Danger!"), bind { |input| Success input.join("-") } # Failure "Danger!"
163
170
  ----
164
171
 
165
- ==== Check
172
+ ===== Check
166
173
 
167
174
  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:
168
175
 
@@ -173,7 +180,7 @@ pipe :a, check(%i[b c], :include?) # Failure :a
173
180
  pipe Failure("Danger!"), check(%i[a b], :include?) # Failure "Danger!"
174
181
  ----
175
182
 
176
- ==== Fmap
183
+ ===== Fmap
177
184
 
178
185
  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:
179
186
 
@@ -183,7 +190,7 @@ pipe %i[a b c], fmap { |input| input.join "-" } # Success "a-b-c"
183
190
  pipe Failure("Danger!"), fmap { |input| input.join "-" } # Failure "Danger!"
184
191
  ----
185
192
 
186
- ==== Insert
193
+ ===== Insert
187
194
 
188
195
  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:
189
196
 
@@ -195,7 +202,7 @@ pipe %i[a c], insert(:b, at: 1) # Success [:a, :b, :c]
195
202
  pipe Failure("Danger!"), insert(:b) # Failure "Danger!"
196
203
  ----
197
204
 
198
- ==== Map
205
+ ===== Map
199
206
 
200
207
  Allows you to map over an enumerable and wraps native link:https://rubyapi.org/o/enumerable#method-i-map[Enumerable#map] functionality.
201
208
 
@@ -205,7 +212,7 @@ pipe %i[a b c], map(&:inspect) # Success [":a", ":b", ":c"]
205
212
  pipe Failure("Danger!"), map(&:inspect) # Failure "Danger!"
206
213
  ----
207
214
 
208
- ==== Merge
215
+ ===== Merge
209
216
 
210
217
  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:
211
218
 
@@ -217,7 +224,7 @@ pipe "test", merge(as: :a, b: 2) # Success {a: "test", b: 2}
217
224
  pipe Failure("Danger!"), merge(b: 2) # Failure "Danger!"
218
225
  ----
219
226
 
220
- ==== Orr
227
+ ===== Orr
221
228
 
222
229
  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.
223
230
 
@@ -232,7 +239,7 @@ pipe Failure("Danger!"), orr { Success "Resolved" } # Success "Reso
232
239
  pipe Failure("Danger!"), orr { |input| Failure "Big #{input}" } # Failure "Big Danger!"
233
240
  ----
234
241
 
235
- ==== Tee
242
+ ===== Tee
236
243
 
237
244
  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:
238
245
 
@@ -249,7 +256,7 @@ pipe Failure("Danger!"), tee(Kernel, :puts, "Example.")
249
256
  # Failure "Danger!"
250
257
  ----
251
258
 
252
- ==== To
259
+ ===== To
253
260
 
254
261
  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:
255
262
 
@@ -265,7 +272,7 @@ pipe({label: "Test"}, to(Model, :for)) # Success #<struct Model label="Test">
265
272
  pipe Failure("Danger!"), to(Model, :for) # Failure "Danger!"
266
273
  ----
267
274
 
268
- ==== Try
275
+ ===== Try
269
276
 
270
277
  Allows you to try an operation which may fail while catching the exception as a failure for further processing. Example:
271
278
 
@@ -276,7 +283,7 @@ pipe "test", try(:invalid, catch: NoMethodError) # Failure "undefined me
276
283
  pipe Failure("Danger!"), try(:to_json, catch: JSON::ParserError) # Failure "Danger!"
277
284
  ----
278
285
 
279
- ==== Use
286
+ ===== Use
280
287
 
281
288
  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.
282
289
 
@@ -288,7 +295,7 @@ pipe 3, use(function) # Success 9
288
295
  pipe Failure("Danger!"), use(function) # Failure "Danger!"
289
296
  ----
290
297
 
291
- ==== Validate
298
+ ===== Validate
292
299
 
293
300
  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.
294
301
 
@@ -303,11 +310,11 @@ pipe({label: "Test"}, validate(schema, as: nil)) # Success #<Dry::Schema::Resul
303
310
  pipe Failure("Danger!"), validate(schema) # Failure "Danger!"
304
311
  ----
305
312
 
306
- === Customization
313
+ ==== Advanced
307
314
 
308
- Should none of the above default steps be to your liking, you have several alternatives available for further customization. Each is described in detail below.
315
+ Several options are available should you need to advance beyond the basic steps. Each is described in detail below.
309
316
 
310
- ==== Procs
317
+ ===== Procs
311
318
 
312
319
  You can always use a `Proc` as a custom step. Example:
313
320
 
@@ -326,7 +333,7 @@ pipe :a,
326
333
 
327
334
  ℹ️ While procs are effective, you are limited in what you can do with them in terms of additional behavior and instrumentation support.
328
335
 
329
- ==== Lambdas
336
+ ===== Lambdas
330
337
 
331
338
  In addition to procs, lambdas can be used too. Example:
332
339
 
@@ -344,7 +351,7 @@ pipe :a,
344
351
 
345
352
  ℹ️ Lambdas are a step up from procs but, like procs, you are limited in what you can do with them in terms of additional behavior and instrumentation support.
346
353
 
347
- ==== Methods
354
+ ===== Methods
348
355
 
349
356
  Methods -- in addition to procs and lambdas -- are the _preferred_ way to add custom steps due to the concise syntax. Example:
350
357
 
@@ -370,9 +377,9 @@ Demo.new.call :a # Yields: Success :a_b
370
377
 
371
378
  ℹ️ You won't be able to instrument these method calls (unless you inject instrumentation) but are great when needing additional behavior between the default steps.
372
379
 
373
- ==== Steps
380
+ ===== Custom
374
381
 
375
- If you'd like to define a more permanent and reusable step, you can register a custom step which requires you to:
382
+ If you'd like to define permanent and reusable step, you can register a custom step which requires you to:
376
383
 
377
384
  . Define a custom step as a new class.
378
385
  . Register your custom step along side the existing default steps.
@@ -386,7 +393,7 @@ module MySteps
386
393
  prepend Transactable::Instrumentable
387
394
 
388
395
  def initialize delimiter = "_", **dependencies
389
- super
396
+ super(**dependencies)
390
397
  @delimiter = delimiter
391
398
  end
392
399
 
@@ -417,9 +424,9 @@ pipe :a,
417
424
  # Yields: Success :ab
418
425
  ----
419
426
 
420
- ==== Containers
427
+ === Containers
421
428
 
422
- Should you not want the default 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 `.with` when including transactable behavior. Example:
429
+ Should you not want the basic steps, need custom steps, or a hybrid of basic and custom steps, you can define your own container and provide it as an argument to `.with` when including transactable behavior. Example:
423
430
 
424
431
  [source,ruby]
425
432
  ----
@@ -488,10 +495,12 @@ There is a lot you can do with instrumentation so check out the {dry_events_link
488
495
 
489
496
  == Development
490
497
 
491
- To set up the project, run:
498
+ To contribute, run:
492
499
 
493
500
  [source,bash]
494
501
  ----
502
+ git clone https://github.com/bkuhlmann/transactable
503
+ cd transactable
495
504
  bin/setup
496
505
  ----
497
506
 
@@ -506,7 +515,7 @@ bin/console
506
515
 
507
516
  The architecture of this gem is built on top of the following concepts and gems:
508
517
 
509
- * *Function Composition*: 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.
518
+ * {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.
510
519
  * {dry_container_link} - Allows related dependencies to be grouped together for injection.
511
520
  * {dry_events_link} - Allows all steps to be observable so you can subscribe to any/all events for metric, logging, and other capabilities.
512
521
  * {dry_monads_link} - Critical to ensuring the entire pipeline of steps adhere to the _Railway Pattern_ and leans heavily on the `Result` object.
@@ -519,34 +528,39 @@ The architecture of this gem is built on top of the following concepts and gems:
519
528
  * *Transactions*
520
529
  ** 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.
521
530
  * *Steps*
522
- ** Inherit from the `Abstract` class in order to gain monad, composition, and dependency behavior. Any dependencies injected are automatically filtered out so all subclasses have direct and clean access to the initial positional, keyword, and block arguments. These variables are prefixed with `initial_*` in order to not conflict with subclasses which might only want to use non-prefixed variables for convenience.
531
+ ** Inherit from the `Abstract` class in order to gain monad, composition, and dependency behavior. Any dependencies injected are automatically filtered out so all subclasses have direct and clean 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.
523
532
  ** 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.
524
533
  ** Prepend `Instrumentable` to gain instrumentation behavior and remain consistent with existing steps. This includes adding the `with instrumentation` RSpec shared context when testing too.
525
534
  ** The `#call` method must define a single positional `result` parameter since a monad will be passed as an argument. Example: `def call(result) = # Implementation`.
526
535
  ** 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 }`.
527
536
  ** 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.
528
537
 
529
- == Tests
538
+ === Debugging
530
539
 
531
- To test, run:
540
+ If you need to debug (i.e. {debug_link}) your pipe, use a lambda. Example:
532
541
 
533
- [source,bash]
542
+ [source,ruby]
534
543
  ----
535
- bundle exec rake
544
+ pipe data,
545
+ check(/Book.+Price/, :match?),
546
+ -> result { binding.break }, # Breakpoint
547
+ method(:parse),
536
548
  ----
537
549
 
538
- == Troubleshooting
550
+ The above breakpoint will allow you inspect the result of the `#check` step and/or build a modified result for passing to the subsequent `#method` step.
551
+
552
+ === Troubleshooting
539
553
 
540
554
  The following might be of aid to as you implement your own transactions.
541
555
 
542
- === Type Errors
556
+ ==== Type Errors
543
557
 
544
558
  If you get a `TypeError: Step must be functionally composable and answer a monad`, it means:
545
559
 
546
560
  . The step must be a `Proc` or some object which responds to `\#>>`, `#<<`, and `#call`.
547
561
  . The step doesn't answer a result monad (i.e. `Success some_value` or `Failure some_value`).
548
562
 
549
- === No Method Errors
563
+ ==== No Method Errors
550
564
 
551
565
  If you get a `NoMethodError: undefined method `success?` exception, it might mean that you forgot to add a comma after one of your steps. Example:
552
566
 
@@ -563,6 +577,15 @@ pipe "https://www.wikipedia.org",
563
577
  try(:parse, catch: HTTP::Error)
564
578
  ----
565
579
 
580
+ == Tests
581
+
582
+ To test, run:
583
+
584
+ [source,bash]
585
+ ----
586
+ bundle exec rake
587
+ ----
588
+
566
589
  == link:https://www.alchemists.io/policies/license[License]
567
590
 
568
591
  == link:https://www.alchemists.io/policies/security[Security]
@@ -8,9 +8,7 @@ module Transactable
8
8
  class Abstract
9
9
  DEPENDENCIES = %i[instrument marameters].freeze
10
10
 
11
- # rubocop:todo Layout/ClassStructure
12
11
  include Import[*DEPENDENCIES]
13
- # rubocop:enable Layout/ClassStructure
14
12
  include Dry::Monads[:result]
15
13
  include Composable
16
14
 
data/transactable.gemspec CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |spec|
4
4
  spec.name = "transactable"
5
- spec.version = "0.0.0"
5
+ spec.version = "0.1.0"
6
6
  spec.authors = ["Brooke Kuhlmann"]
7
7
  spec.email = ["brooke@alchemists.io"]
8
8
  spec.homepage = "https://www.alchemists.io/projects/transactable"
@@ -24,7 +24,7 @@ Gem::Specification.new do |spec|
24
24
 
25
25
  spec.required_ruby_version = "~> 3.1"
26
26
 
27
- spec.add_dependency "dry-container", "~> 0.10"
27
+ spec.add_dependency "dry-container", "~> 0.11"
28
28
  spec.add_dependency "dry-events", "~> 0.3"
29
29
  spec.add_dependency "dry-monads", "~> 1.4"
30
30
  spec.add_dependency "infusible", "~> 0.0"
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: transactable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brooke Kuhlmann
@@ -28,7 +28,7 @@ cert_chain:
28
28
  CxDe2+VuChj4I1nvIHdu+E6XoEVlanUPKmSg6nddhkKn2gC45Kyzh6FZqnzH/CRp
29
29
  RFE=
30
30
  -----END CERTIFICATE-----
31
- date: 2022-09-10 00:00:00.000000000 Z
31
+ date: 2022-09-17 00:00:00.000000000 Z
32
32
  dependencies:
33
33
  - !ruby/object:Gem::Dependency
34
34
  name: dry-container
@@ -36,14 +36,14 @@ dependencies:
36
36
  requirements:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: '0.10'
39
+ version: '0.11'
40
40
  type: :runtime
41
41
  prerelease: false
42
42
  version_requirements: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: '0.10'
46
+ version: '0.11'
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: dry-events
49
49
  requirement: !ruby/object:Gem::Requirement
metadata.gz.sig CHANGED
Binary file