transactable 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.adoc ADDED
@@ -0,0 +1,581 @@
1
+ :command_pattern_link: link:https://www.alchemists.io/articles/command_pattern[Command Pattern]
2
+ :dry_container_link: link:https://dry-rb.org/gems/dry-container[Dry Container]
3
+ :dry_events_link: link:https://dry-rb.org/gems/dry-events[Dry Events]
4
+ :dry_monads_link: link:https://dry-rb.org/gems/dry-monads[Dry Monads]
5
+ :dry_schema_link: link:https://dry-rb.org/gems/dry-schema[Dry Schema]
6
+ :dry_validation_link: link:https://dry-rb.org/gems/dry-validation[Dry Validation]
7
+
8
+ :toc: macro
9
+ :toclevels: 5
10
+ :figure-caption!:
11
+
12
+ = Transactable
13
+
14
+ 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.
15
+
16
+ toc::[]
17
+
18
+ == Features
19
+
20
+ * Built on top of native function composition capabilities.
21
+ * Customizable with additional or entirely different steps.
22
+ * Chainable where you can couple together multiple transactions to build more complex architectures.
23
+ * Instrumentable so you can track metrics, log usage, and much more.
24
+
25
+ == Requirements
26
+
27
+ . link:https://www.ruby-lang.org[Ruby].
28
+
29
+ == Setup
30
+
31
+ To install, run:
32
+
33
+ [source,bash]
34
+ ----
35
+ gem install transactable
36
+ ----
37
+
38
+ Add the following to your Gemfile file:
39
+
40
+ [source,ruby]
41
+ ----
42
+ gem "transactable"
43
+ ----
44
+
45
+ == Usage
46
+
47
+ You can turn any object into a _transaction_ by requiring and including this gem as follows:
48
+
49
+ [source,ruby]
50
+ ----
51
+ require "csv"
52
+ require "transactable"
53
+
54
+ class Demo
55
+ include Transactable
56
+
57
+ def initialize client: CSV
58
+ @client = client
59
+ end
60
+
61
+ def call data
62
+ pipe data,
63
+ check(/Book.+Price/, :match?),
64
+ method(:parse),
65
+ map { |item| "#{item[:book]}: #{item[:price]}" }
66
+ end
67
+
68
+ private
69
+
70
+ attr_reader :client
71
+
72
+ def parse result
73
+ result.fmap do |data|
74
+ client.instance(data, headers: true, header_converters: proc { |key| key.downcase.to_sym })
75
+ .to_a
76
+ .map(&:to_h)
77
+ end
78
+ end
79
+ end
80
+ ----
81
+
82
+ The above allows the `Demo#call` method to be a _transactional_ series of composed steps which may pass or fail where each step is piped together via {dry_monads_link}. This is the essence of the _Railway Pattern_.
83
+
84
+ To execute the above example, you'd only need to pass CSV content to it:
85
+
86
+ [source,ruby]
87
+ ----
88
+ csv = <<~CSV
89
+ Book,Author,Price,At
90
+ Mystics,urGoh,10.50,2022-01-01
91
+ Skeksis,skekSil,20.75,2022-02-13
92
+ CSV
93
+
94
+ demo = Demo.new
95
+
96
+ demo.call csv
97
+ ----
98
+
99
+ The computed result is a success with each book listing a price:
100
+
101
+ ....
102
+ Success ["Mystics: 10.50", "Skeksis: 20.75"]
103
+ ....
104
+
105
+ === Pipe
106
+
107
+ Once you've included the `Transactable` module within your class, the `#pipe` method is available to you and is how you build a series of steps for processing. The method signature is:
108
+
109
+ [source,ruby]
110
+ ----
111
+ pipe(input, *steps)
112
+ ----
113
+
114
+ 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_.
115
+
116
+ Behind the scenes, the `#pipe` method is syntactic sugar on top of function composition which means if this code were to be rewritten:
117
+
118
+ [source,ruby]
119
+ ----
120
+ pipe csv,
121
+ check(/Book.+Price/, :match?),
122
+ method(:parse),
123
+ map { |item| "#{item[:book]}: #{item[:price]}" }
124
+ ----
125
+
126
+ Then the above would look like this using native Ruby:
127
+
128
+ [source,ruby]
129
+ ----
130
+ (
131
+ check(/Book.+Price/, :match?) >>
132
+ method(:parse) >>
133
+ map { |item| "#{item[:book]}: #{item[:price]}" }
134
+ ).call Success(csv)
135
+ ----
136
+
137
+ The only problem with native function composition is that it reads backwards by passing in 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.
138
+
139
+ === Steps
140
+
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.
142
+
143
+ ==== As
144
+
145
+ Allows you to message the input as different output. Example:
146
+
147
+ [source,ruby]
148
+ ----
149
+ pipe :a, as(:inspect) # Success ":a"
150
+ pipe %i[a b c], as(:dig, 1) # Success :b
151
+ pipe Failure("Danger!"), as(:inspect) # Failure "Danger!"
152
+ ----
153
+
154
+ ==== Bind
155
+
156
+ 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
+
158
+ [source,ruby]
159
+ ----
160
+ pipe %i[a b c], bind { |input| Success input.join("-") } # Success "a-b-c"
161
+ pipe %i[a b c], bind { |input| Failure input } # Failure [:a, :b, :c]
162
+ pipe Failure("Danger!"), bind { |input| Success input.join("-") } # Failure "Danger!"
163
+ ----
164
+
165
+ ==== Check
166
+
167
+ 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
+
169
+ [source,ruby]
170
+ ----
171
+ pipe :a, check(%i[a b], :include?) # Success :a
172
+ pipe :a, check(%i[b c], :include?) # Failure :a
173
+ pipe Failure("Danger!"), check(%i[a b], :include?) # Failure "Danger!"
174
+ ----
175
+
176
+ ==== Fmap
177
+
178
+ 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
+
180
+ [source,ruby]
181
+ ----
182
+ pipe %i[a b c], fmap { |input| input.join "-" } # Success "a-b-c"
183
+ pipe Failure("Danger!"), fmap { |input| input.join "-" } # Failure "Danger!"
184
+ ----
185
+
186
+ ==== Insert
187
+
188
+ 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
+
190
+ [source,ruby]
191
+ ----
192
+ pipe :a, insert(:b) # Success [:a, :b]
193
+ pipe :a, insert(:b, at: 0) # Success [:b, :a]
194
+ pipe %i[a c], insert(:b, at: 1) # Success [:a, :b, :c]
195
+ pipe Failure("Danger!"), insert(:b) # Failure "Danger!"
196
+ ----
197
+
198
+ ==== Map
199
+
200
+ Allows you to map over an enumerable and wraps native link:https://rubyapi.org/o/enumerable#method-i-map[Enumerable#map] functionality.
201
+
202
+ [source,ruby]
203
+ ----
204
+ pipe %i[a b c], map(&:inspect) # Success [":a", ":b", ":c"]
205
+ pipe Failure("Danger!"), map(&:inspect) # Failure "Danger!"
206
+ ----
207
+
208
+ ==== Merge
209
+
210
+ 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
+
212
+ [source,ruby]
213
+ ----
214
+ pipe({a: 1}, merge(b: 2)) # Success {a: 1, b: 2}
215
+ pipe "test", merge(b: 2) # Success {step: "test", b: 2}
216
+ pipe "test", merge(as: :a, b: 2) # Success {a: "test", b: 2}
217
+ pipe Failure("Danger!"), merge(b: 2) # Failure "Danger!"
218
+ ----
219
+
220
+ ==== Orr
221
+
222
+ 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
+
224
+ ℹ️ Syntactically, `or` can't be used for this step since `or` is a native Ruby keyword so `orr` is used instead.
225
+
226
+ Example:
227
+
228
+ [source,ruby]
229
+ ----
230
+ pipe %i[a b c], orr { |input| Success input.join("-") } # Success [:a, :b, :c]
231
+ pipe Failure("Danger!"), orr { Success "Resolved" } # Success "Resolved"
232
+ pipe Failure("Danger!"), orr { |input| Failure "Big #{input}" } # Failure "Big Danger!"
233
+ ----
234
+
235
+ ==== Tee
236
+
237
+ 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
+
239
+ [source,ruby]
240
+ ----
241
+ pipe "test", tee(Kernel, :puts, "Example.")
242
+
243
+ # Example.
244
+ # Success "test"
245
+
246
+ pipe Failure("Danger!"), tee(Kernel, :puts, "Example.")
247
+
248
+ # Example.
249
+ # Failure "Danger!"
250
+ ----
251
+
252
+ ==== To
253
+
254
+ 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
+
256
+ [source,ruby]
257
+ ----
258
+ Model = Struct.new :label, keyword_init: true do
259
+ include Dry::Monads[:result]
260
+
261
+ def self.for(...) = Success new(...)
262
+ end
263
+
264
+ pipe({label: "Test"}, to(Model, :for)) # Success #<struct Model label="Test">
265
+ pipe Failure("Danger!"), to(Model, :for) # Failure "Danger!"
266
+ ----
267
+
268
+ ==== Try
269
+
270
+ Allows you to try an operation which may fail while catching the exception as a failure for further processing. Example:
271
+
272
+ [source,ruby]
273
+ ----
274
+ pipe "test", try(:to_json, catch: JSON::ParserError) # Success "\"test\""
275
+ pipe "test", try(:invalid, catch: NoMethodError) # Failure "undefined method..."
276
+ pipe Failure("Danger!"), try(:to_json, catch: JSON::ParserError) # Failure "Danger!"
277
+ ----
278
+
279
+ ==== Use
280
+
281
+ 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
+
283
+ [source,ruby]
284
+ ----
285
+ function = -> input { Success input * 3 }
286
+
287
+ pipe 3, use(function) # Success 9
288
+ pipe Failure("Danger!"), use(function) # Failure "Danger!"
289
+ ----
290
+
291
+ ==== Validate
292
+
293
+ 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
+
295
+ 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.
296
+
297
+ [source,ruby]
298
+ ----
299
+ schema = Dry::Schema.Params { required(:label).filled :string }
300
+
301
+ pipe({label: "Test"}, validate(schema)) # Success label: "Test"
302
+ pipe({label: "Test"}, validate(schema, as: nil)) # Success #<Dry::Schema::Result{:label=>"Test"} errors={} path=[]>
303
+ pipe Failure("Danger!"), validate(schema) # Failure "Danger!"
304
+ ----
305
+
306
+ === Customization
307
+
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.
309
+
310
+ ==== Procs
311
+
312
+ You can always use a `Proc` as a custom step. Example:
313
+
314
+ [source,ruby]
315
+ ----
316
+ include Transactable
317
+ include Dry::Monads[:result]
318
+
319
+ pipe :a,
320
+ insert(:b),
321
+ proc { Success "input_ignored" },
322
+ as(:to_sym)
323
+
324
+ # Yields: Success :input_ignored
325
+ ----
326
+
327
+ ℹ️ While procs are effective, you are limited in what you can do with them in terms of additional behavior and instrumentation support.
328
+
329
+ ==== Lambdas
330
+
331
+ In addition to procs, lambdas can be used too. Example:
332
+
333
+ [source,ruby]
334
+ ----
335
+ include Transactable
336
+
337
+ pipe :a,
338
+ insert(:b),
339
+ -> result { result.fmap { |input| input.join "_" } },
340
+ as(:to_sym)
341
+
342
+ # Yields: Success :a_b
343
+ ----
344
+
345
+ ℹ️ 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
+
347
+ ==== Methods
348
+
349
+ Methods -- in addition to procs and lambdas -- are the _preferred_ way to add custom steps due to the concise syntax. Example:
350
+
351
+ [source,ruby]
352
+ ----
353
+ class Demo
354
+ include Transactable
355
+
356
+ def call input
357
+ pipe :a,
358
+ insert(:b),
359
+ method(:join),
360
+ as(:to_sym)
361
+ end
362
+
363
+ private
364
+
365
+ def join(result) = result.fmap { |input| input.join "_" }
366
+ end
367
+
368
+ Demo.new.call :a # Yields: Success :a_b
369
+ ----
370
+
371
+ ℹ️ 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
+
373
+ ==== Steps
374
+
375
+ If you'd like to define a more permanent and reusable step, you can register a custom step which requires you to:
376
+
377
+ . Define a custom step as a new class.
378
+ . Register your custom step along side the existing default steps.
379
+
380
+ Here's what this would look like:
381
+
382
+ [source,ruby]
383
+ ----
384
+ module MySteps
385
+ class Join < Transactable::Steps::Abstract
386
+ prepend Transactable::Instrumentable
387
+
388
+ def initialize delimiter = "_", **dependencies
389
+ super
390
+ @delimiter = delimiter
391
+ end
392
+
393
+ def call(result) = result.fmap { |input| input.join delimiter }
394
+
395
+ private
396
+
397
+ attr_reader :delimiter
398
+ end
399
+ end
400
+
401
+ Transactable::Steps::Container.register(:join) { MySteps::Join }
402
+
403
+ include Transactable
404
+
405
+ pipe :a,
406
+ insert(:b),
407
+ join,
408
+ as(:to_sym)
409
+
410
+ # Yields: Success :a_b
411
+
412
+ pipe :a,
413
+ insert(:b),
414
+ join(""),
415
+ as(:to_sym)
416
+
417
+ # Yields: Success :ab
418
+ ----
419
+
420
+ ==== Containers
421
+
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:
423
+
424
+ [source,ruby]
425
+ ----
426
+ require "dry/container"
427
+
428
+ module MyContainer
429
+ extend Dry::Container::Mixin
430
+
431
+ register :echo, -> result { result }
432
+ register(:insert) { Transactable::Steps::Insert }
433
+ end
434
+
435
+ include Transactable.with(MyContainer)
436
+
437
+ pipe :a, echo, insert(:b)
438
+
439
+ # Yields: Success [:a, :b]
440
+ ----
441
+
442
+ The above is a hybrid example where the `MyContainer` 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 `.with` (i.e. `include Transactable.with(MyContainer)`).
443
+
444
+ Whether you use default, custom, or hybrid steps, you have maximum flexibility using this approach.
445
+
446
+ === Composition
447
+
448
+ Should you ever need to make a plain old Ruby object functionally composable, then you can _include_ the `Transactable::Composable` module which will give you the necessary `\#>>`, `#<<`, and `#call` methods where you only need to implement the `#call` method.
449
+
450
+ === Instrumentation
451
+
452
+ Each transaction includes instrumentation using {dry_events_link} which you can subscribe to or ignore entirely. The following events are supported:
453
+
454
+ * `step`: Published for each step regardless of success or failure.
455
+ * `step.success`: Published for success steps only.
456
+ * `step.failure`: Published for failure steps only.
457
+
458
+ Using the example code at the start of this _Usage_ section, here's how you can subscribe to events emitted by the transaction:
459
+
460
+ [source,ruby]
461
+ ----
462
+ Transactable::Instrument::EVENTS.each do |name|
463
+ Transactable::Container[:instrument].subscribe name do |event|
464
+ puts "#{event.id}: #{event.payload}"
465
+ end
466
+ end
467
+ ----
468
+
469
+ Now, as before, you can call the transaction with subscribers enabled:
470
+
471
+ [source,ruby]
472
+ ----
473
+ demo.call csv
474
+ ----
475
+
476
+ The above will then yield the following results in your console:
477
+
478
+ ....
479
+ step: {:name=>"Transactable::Steps::Check", :arguments=>[[], {}, nil]}
480
+ step.success: {:name=>"Transactable::Steps::Check", :value=>"Book,Author,Price,At\nMystics,urGoh,10.50,2022-01-01\nSkeksis,skekSil,20.75,2022-02-13\n", :arguments=>[[], {}, nil]}
481
+ step: {:name=>"Transactable::Steps::Map", :arguments=>[[], {}, #<Proc:0x0000000106405900 (irb):15>]}
482
+ step.success: {:name=>"Transactable::Steps::Map", :value=>["Mystics: 10.50", "Skeksis: 20.75"], :arguments=>[[], {}, #<Proc:0x0000000106405900 (irb):15>]}
483
+ ....
484
+
485
+ Finally, the `Transactable::Instrumentable` module is available should you need to _prepend_ instrumentation to any of your classes.
486
+
487
+ There is a lot you can do with instrumentation so check out the {dry_events_link} documentation for further details.
488
+
489
+ == Development
490
+
491
+ To set up the project, run:
492
+
493
+ [source,bash]
494
+ ----
495
+ bin/setup
496
+ ----
497
+
498
+ You can also use the IRB console for direct access to all objects:
499
+
500
+ [source,bash]
501
+ ----
502
+ bin/console
503
+ ----
504
+
505
+ === Architecture
506
+
507
+ The architecture of this gem is built on top of the following concepts and gems:
508
+
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.
510
+ * {dry_container_link} - Allows related dependencies to be grouped together for injection.
511
+ * {dry_events_link} - Allows all steps to be observable so you can subscribe to any/all events for metric, logging, and other capabilities.
512
+ * {dry_monads_link} - Critical to ensuring the entire pipeline of steps adhere to the _Railway Pattern_ and leans heavily on the `Result` object.
513
+ * link:https://dry-rb.org/gems/dry-transaction[Dry Transaction] - Specifically the concept of a _step_ where each step can have an _operation_ and/or _input_ to be processed. Instrumentation is used as well so you can have rich metrics, logging, or any other kind of observer wired up as desired.
514
+ * link:https://www.alchemists.io/projects/infusible[Infusible] - Coupled with {dry_container_link}, allows dependencies to be automatically injected.
515
+ * link:https://www.alchemists.io/projects/marameters[Marameters] - Through the use of the `.categorize` method, dynamic message passing is possible by inspecting the operation method's parameters.
516
+
517
+ === Style Guide
518
+
519
+ * *Transactions*
520
+ ** 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
+ * *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.
523
+ ** 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
+ ** 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
+ ** 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
+ ** 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
+ ** 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
+
529
+ == Tests
530
+
531
+ To test, run:
532
+
533
+ [source,bash]
534
+ ----
535
+ bundle exec rake
536
+ ----
537
+
538
+ == Troubleshooting
539
+
540
+ The following might be of aid to as you implement your own transactions.
541
+
542
+ === Type Errors
543
+
544
+ If you get a `TypeError: Step must be functionally composable and answer a monad`, it means:
545
+
546
+ . The step must be a `Proc` or some object which responds to `\#>>`, `#<<`, and `#call`.
547
+ . The step doesn't answer a result monad (i.e. `Success some_value` or `Failure some_value`).
548
+
549
+ === No Method Errors
550
+
551
+ 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
+
553
+ [source,ruby]
554
+ ----
555
+ # Valid
556
+ pipe "https://www.wikipedia.org",
557
+ to(client, :get),
558
+ try(:parse, catch: HTTP::Error)
559
+
560
+ # Invalid
561
+ pipe "https://www.wikipedia.org",
562
+ to(client, :get) # <= Comma is missing on this line.
563
+ try(:parse, catch: HTTP::Error)
564
+ ----
565
+
566
+ == link:https://www.alchemists.io/policies/license[License]
567
+
568
+ == link:https://www.alchemists.io/policies/security[Security]
569
+
570
+ == link:https://www.alchemists.io/policies/code_of_conduct[Code of Conduct]
571
+
572
+ == link:https://www.alchemists.io/policies/contributions[Contributions]
573
+
574
+ == link:https://www.alchemists.io/projects/transactable/versions[Versions]
575
+
576
+ == link:https://www.alchemists.io/community[Community]
577
+
578
+ == Credits
579
+
580
+ * Built with link:https://www.alchemists.io/projects/gemsmith[Gemsmith].
581
+ * Engineered by link:https://www.alchemists.io/team/brooke_kuhlmann[Brooke Kuhlmann].
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Transactable
4
+ # Allows objects to be functionally composable.
5
+ module Composable
6
+ def >>(other) = method(:call) >> other
7
+
8
+ def <<(other) = method(:call) << other
9
+
10
+ def call = fail NotImplementedError, "`#{self.class.name}##{__method__}` must be implemented."
11
+ end
12
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/container"
4
+ require "marameters"
5
+
6
+ module Transactable
7
+ # Provides a global container of common objects.
8
+ module Container
9
+ extend Dry::Container::Mixin
10
+
11
+ register(:instrument, memoize: true) { Instrument.new }
12
+ register(:marameters) { Marameters }
13
+ end
14
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "infusible"
4
+
5
+ module Transactable
6
+ Import = Infusible.with Container
7
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/events"
4
+
5
+ module Transactable
6
+ # Provides a common instrument for subscribing to and publishing of events.
7
+ class Instrument
8
+ include Dry::Events::Publisher[name]
9
+
10
+ EVENTS = %w[step step.success step.failure].freeze
11
+
12
+ def initialize events: EVENTS
13
+ events.each { |name| register_event name }
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/monads"
4
+
5
+ module Transactable
6
+ # Allows a callable to have instrumentation. This module is meant to be prepended only.
7
+ module Instrumentable
8
+ def call(...)
9
+ arguments = [base_positionals, base_keywords, base_block]
10
+
11
+ instrument.publish("step", name: self.class.name, arguments:)
12
+
13
+ super.fmap { |value| publish_success value, arguments }
14
+ .or { |value| publish_failure value, arguments }
15
+ end
16
+
17
+ module_function
18
+
19
+ def publish_success value, arguments
20
+ instrument.publish("step.success", name: self.class.name, value:, arguments:)
21
+ value
22
+ end
23
+
24
+ def publish_failure value, arguments
25
+ instrument.publish("step.failure", name: self.class.name, value:, arguments:)
26
+ Failure value
27
+ end
28
+ end
29
+ end