transactable 0.5.2 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/README.adoc +33 -41
- data/lib/transactable/pipe.rb +16 -0
- data/lib/transactable/pipeable.rb +12 -9
- data/lib/transactable/steps/abstract.rb +5 -0
- data/lib/transactable/steps/as.rb +0 -2
- data/lib/transactable/steps/bind.rb +0 -2
- data/lib/transactable/steps/check.rb +0 -2
- data/lib/transactable/steps/fmap.rb +0 -2
- data/lib/transactable/steps/insert.rb +0 -2
- data/lib/transactable/steps/map.rb +0 -2
- data/lib/transactable/steps/merge.rb +0 -2
- data/lib/transactable/steps/or.rb +0 -2
- data/lib/transactable/steps/tee.rb +0 -2
- data/lib/transactable/steps/to.rb +0 -2
- data/lib/transactable/steps/try.rb +0 -2
- data/lib/transactable/steps/use.rb +0 -2
- data/lib/transactable/steps/validate.rb +0 -2
- data/lib/transactable.rb +1 -1
- data/transactable.gemspec +1 -1
- data.tar.gz.sig +0 -0
- metadata +4 -3
- metadata.gz.sig +1 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d17683ad67f8fca4a79b13497ad7e19c46b1dd0614783e4296cbe551bae7f6be
|
4
|
+
data.tar.gz: 22c0954b70991d59a4637548580f692db6413236e35393b5d352bc8ef9acebee
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 26348144c79119b8eeb8748ca30f6af243a1bb8f554bfda111dbcf037440338573c14610ae7138b977edbac4db8375cfd9797409012a871087ee5c6aa51eab4a
|
7
|
+
data.tar.gz: 7ade67cf3843ad63694575e27269269fe5cb4043673465a254095a8468c6cf59e74a1e7455d6e34aa63a0934b4ab380ece8cbd3971ee2124f660d551e3b715bf
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data/README.adoc
CHANGED
@@ -3,26 +3,31 @@
|
|
3
3
|
:figure-caption!:
|
4
4
|
|
5
5
|
:command_pattern_link: link:https://alchemists.io/articles/command_pattern[Command Pattern]
|
6
|
-
:function_composition_link: link:https://alchemists.io/articles/ruby_function_composition[Function Composition]
|
7
6
|
:debug_link: link:https://github.com/ruby/debug[Debug]
|
8
7
|
:dry_container_link: link:https://dry-rb.org/gems/dry-container[Dry Container]
|
9
8
|
:dry_events_link: link:https://dry-rb.org/gems/dry-events[Dry Events]
|
10
9
|
:dry_monads_link: link:https://dry-rb.org/gems/dry-monads[Dry Monads]
|
11
10
|
:dry_schema_link: link:https://dry-rb.org/gems/dry-schema[Dry Schema]
|
12
11
|
:dry_validation_link: link:https://dry-rb.org/gems/dry-validation[Dry Validation]
|
12
|
+
:function_composition_link: link:https://alchemists.io/articles/ruby_function_composition[Function Composition]
|
13
|
+
:infusible_link: link:https://alchemists.io/projects/infusible[Infusible]
|
14
|
+
:railway_pattern_link: link:https://fsharpforfunandprofit.com/posts/recipe-part2[Railway Pattern]
|
13
15
|
|
14
16
|
= Transactable
|
15
17
|
|
16
|
-
A DSL for transactional workflows built atop
|
18
|
+
A DSL for transactional 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.
|
17
19
|
|
18
20
|
toc::[]
|
19
21
|
|
20
22
|
== Features
|
21
23
|
|
22
|
-
* Built
|
23
|
-
*
|
24
|
-
*
|
25
|
-
*
|
24
|
+
* Built atop of native {function_composition_link}.
|
25
|
+
* Adheres to the {railway_pattern_link}.
|
26
|
+
* Provides built-in and customizable domain-specific steps.
|
27
|
+
* Provides chainable _pipes_ which can be used to build more complex workflows.
|
28
|
+
* Supports instrumentation for tracking metrics, logging usage, and much more.
|
29
|
+
* Compatible with {dry_monads_link}.
|
30
|
+
* Compatible with {infusible_link}.
|
26
31
|
|
27
32
|
== Requirements
|
28
33
|
|
@@ -80,7 +85,7 @@ class Demo
|
|
80
85
|
def call data
|
81
86
|
pipe data,
|
82
87
|
check(/Book.+Price/, :match?),
|
83
|
-
|
88
|
+
:parse,
|
84
89
|
map { |item| "#{item[:book]}: #{item[:price]}" }
|
85
90
|
end
|
86
91
|
|
@@ -98,21 +103,17 @@ class Demo
|
|
98
103
|
end
|
99
104
|
----
|
100
105
|
|
101
|
-
The above allows
|
106
|
+
The above allows `Demo#call` to be a _transactional_ sequence steps which may pass or fail due to all step being {dry_monads_link}. This is the essence of the {railway_pattern_link}.
|
102
107
|
|
103
108
|
To execute the above example, you'd only need to pass CSV content to it:
|
104
109
|
|
105
110
|
[source,ruby]
|
106
111
|
----
|
107
|
-
|
112
|
+
Demo.new.call <<~CSV
|
108
113
|
Book,Author,Price,At
|
109
114
|
Mystics,urGoh,10.50,2022-01-01
|
110
115
|
Skeksis,skekSil,20.75,2022-02-13
|
111
116
|
CSV
|
112
|
-
|
113
|
-
demo = Demo.new
|
114
|
-
|
115
|
-
demo.call csv
|
116
117
|
----
|
117
118
|
|
118
119
|
The computed result is a success with each book listing a price:
|
@@ -123,22 +124,22 @@ Success ["Mystics: 10.50", "Skeksis: 20.75"]
|
|
123
124
|
|
124
125
|
=== Pipe
|
125
126
|
|
126
|
-
Once you've included the `Transactable` module within your class, the `#pipe` method is available to you and is how you build a
|
127
|
+
Once you've included the `Transactable` module within your class, the `#pipe` method is available to you and is how you build a sequence of steps for processing. The method signature is:
|
127
128
|
|
128
129
|
[source,ruby]
|
129
130
|
----
|
130
131
|
pipe(input, *steps)
|
131
132
|
----
|
132
133
|
|
133
|
-
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
|
134
|
+
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}.
|
134
135
|
|
135
|
-
Behind the scenes, the `#pipe` method is syntactic sugar on top of
|
136
|
+
Behind the scenes, the `#pipe` method is syntactic sugar on top of {function_composition_link} which means if this code were to be rewritten:
|
136
137
|
|
137
138
|
[source,ruby]
|
138
139
|
----
|
139
140
|
pipe csv,
|
140
141
|
check(/Book.+Price/, :match?),
|
141
|
-
|
142
|
+
:parse,
|
142
143
|
map { |item| "#{item[:book]}: #{item[:price]}" }
|
143
144
|
----
|
144
145
|
|
@@ -153,7 +154,7 @@ Then the above would look like this using native Ruby:
|
|
153
154
|
).call Success(csv)
|
154
155
|
----
|
155
156
|
|
156
|
-
The
|
157
|
+
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.
|
157
158
|
|
158
159
|
=== Steps
|
159
160
|
|
@@ -379,7 +380,7 @@ class Demo
|
|
379
380
|
def call input
|
380
381
|
pipe :a,
|
381
382
|
insert(:b),
|
382
|
-
|
383
|
+
:join,
|
383
384
|
as(:to_sym)
|
384
385
|
end
|
385
386
|
|
@@ -391,6 +392,8 @@ end
|
|
391
392
|
Demo.new.call :a # Yields: Success :a_b
|
392
393
|
----
|
393
394
|
|
395
|
+
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.
|
396
|
+
|
394
397
|
ℹ️ 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.
|
395
398
|
|
396
399
|
===== Custom
|
@@ -406,8 +409,6 @@ Here's what this would look like:
|
|
406
409
|
----
|
407
410
|
module MySteps
|
408
411
|
class Join < Transactable::Steps::Abstract
|
409
|
-
prepend Transactable::Instrumentable
|
410
|
-
|
411
412
|
def initialize(delimiter = "_", **)
|
412
413
|
super(**)
|
413
414
|
@delimiter = delimiter
|
@@ -425,18 +426,10 @@ Transactable::Steps::Container.register(:join) { MySteps::Join }
|
|
425
426
|
|
426
427
|
include Transactable
|
427
428
|
|
428
|
-
pipe :a,
|
429
|
-
insert(:b),
|
430
|
-
join,
|
431
|
-
as(:to_sym)
|
432
|
-
|
429
|
+
pipe :a, insert(:b), join, as(:to_sym)
|
433
430
|
# Yields: Success :a_b
|
434
431
|
|
435
|
-
pipe :a,
|
436
|
-
insert(:b),
|
437
|
-
join(""),
|
438
|
-
as(:to_sym)
|
439
|
-
|
432
|
+
pipe :a, insert(:b), join(""), as(:to_sym)
|
440
433
|
# Yields: Success :ab
|
441
434
|
----
|
442
435
|
|
@@ -505,7 +498,7 @@ step: {:name=>"Transactable::Steps::Map", :arguments=>[[], {}, #<Proc:0x00000001
|
|
505
498
|
step.success: {:name=>"Transactable::Steps::Map", :value=>["Mystics: 10.50", "Skeksis: 20.75"], :arguments=>[[], {}, #<Proc:0x0000000106405900 (irb):15>]}
|
506
499
|
....
|
507
500
|
|
508
|
-
Finally, the `Transactable::Instrumentable` module is available should you need to _prepend_ instrumentation to any of your
|
501
|
+
Finally, the `Transactable::Instrumentable` module is available should you need to _prepend_ instrumentation to any of your class' `#call` methods.
|
509
502
|
|
510
503
|
There is a lot you can do with instrumentation so check out the {dry_events_link} documentation for further details.
|
511
504
|
|
@@ -532,12 +525,12 @@ bin/console
|
|
532
525
|
The architecture of this gem is built on top of the following concepts and gems:
|
533
526
|
|
534
527
|
* {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.
|
535
|
-
* {dry_container_link}
|
536
|
-
* {dry_events_link}
|
537
|
-
* {dry_monads_link}
|
538
|
-
* link:https://dry-rb.org/gems/dry-transaction[Dry Transaction]
|
539
|
-
* link:https://alchemists.io/projects/infusible[Infusible]
|
540
|
-
* link:https://alchemists.io/projects/marameters[Marameters]
|
528
|
+
* {dry_container_link}: Allows related dependencies to be grouped together for injection as desired.
|
529
|
+
* {dry_events_link}: Allows all steps to be observable so you can subscribe to any/all events for metric, logging, and other capabilities.
|
530
|
+
* {dry_monads_link}: Critical to ensuring the entire pipeline of steps adhere to the {railway_pattern_link} and leans heavily on the `Result` object.
|
531
|
+
* 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.
|
532
|
+
* link:https://alchemists.io/projects/infusible[Infusible]: Coupled with {dry_container_link}, allows dependencies to be automatically injected.
|
533
|
+
* 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.
|
541
534
|
|
542
535
|
=== Style Guide
|
543
536
|
|
@@ -546,7 +539,6 @@ The architecture of this gem is built on top of the following concepts and gems:
|
|
546
539
|
* *Steps*
|
547
540
|
** 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.
|
548
541
|
** 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.
|
549
|
-
** Prepend `Instrumentable` to gain instrumentation behavior and remain consistent with existing steps. This includes adding the `with instrumentation` RSpec shared context when testing too.
|
550
542
|
** The `#call` method must define a single positional `result` parameter since a monad will be passed as an argument. Example: `def call(result) = # Implementation`.
|
551
543
|
** 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 }`.
|
552
544
|
** 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.
|
@@ -560,7 +552,7 @@ If you need to debug (i.e. {debug_link}) your pipe, use a lambda. Example:
|
|
560
552
|
pipe data,
|
561
553
|
check(/Book.+Price/, :match?),
|
562
554
|
-> result { binding.break }, # Breakpoint
|
563
|
-
|
555
|
+
:parse
|
564
556
|
----
|
565
557
|
|
566
558
|
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.
|
@@ -573,7 +565,7 @@ The following might be of aid to as you implement your own transactions.
|
|
573
565
|
|
574
566
|
If you get a `TypeError: Step must be functionally composable and answer a monad`, it means:
|
575
567
|
|
576
|
-
. The step must be a `Proc` or some object which responds to `\#>>`, `#<<`, and `#call`.
|
568
|
+
. The step must be a `Proc`, `Method`, or some object which responds to `\#>>`, `#<<`, and `#call`.
|
577
569
|
. The step doesn't answer a result monad (i.e. `Success some_value` or `Failure some_value`).
|
578
570
|
|
579
571
|
==== No Method Errors
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/monads"
|
4
|
+
|
5
|
+
module Transactable
|
6
|
+
# Provids low-level functionality that can process a sequence of steps.
|
7
|
+
Pipe = lambda do |input, *steps|
|
8
|
+
fail ArgumentError, "Pipe must have at least one step." if steps.empty?
|
9
|
+
|
10
|
+
result = input.is_a?(Dry::Monads::Result) ? input : Dry::Monads::Success(input)
|
11
|
+
|
12
|
+
steps.reduce(&:>>).call result
|
13
|
+
rescue NoMethodError
|
14
|
+
raise TypeError, "Step must be functionally composable and answer a monad."
|
15
|
+
end
|
16
|
+
end
|
@@ -1,13 +1,19 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "dry/monads"
|
4
|
+
require "refinements/arrays"
|
4
5
|
|
5
6
|
module Transactable
|
6
7
|
# Allows any object to pipe sequential steps together which can be composed into a single result.
|
7
8
|
class Pipeable < Module
|
8
|
-
|
9
|
+
include Dry::Monads[:result]
|
10
|
+
|
11
|
+
using Refinements::Arrays
|
12
|
+
|
13
|
+
def initialize steps = Steps::Container, pipe: Pipe
|
9
14
|
super()
|
10
15
|
@steps = steps
|
16
|
+
@pipe = pipe
|
11
17
|
@instance_module = Class.new(Module).new
|
12
18
|
end
|
13
19
|
|
@@ -20,17 +26,14 @@ module Transactable
|
|
20
26
|
|
21
27
|
private
|
22
28
|
|
23
|
-
attr_reader :steps, :instance_module
|
29
|
+
attr_reader :steps, :pipe, :instance_module
|
24
30
|
|
25
31
|
def define_pipe
|
26
|
-
|
27
|
-
fail ArgumentError, "Transaction must have at least one step." if steps.empty?
|
32
|
+
local_pipe = pipe
|
28
33
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
rescue NoMethodError
|
33
|
-
raise TypeError, "Step must be functionally composable and answer a monad."
|
34
|
+
instance_module.define_method :pipe do |input, *steps|
|
35
|
+
steps.each { |step| steps.supplant step, method(step) if step.is_a? Symbol }
|
36
|
+
local_pipe.call(input, *steps)
|
34
37
|
end
|
35
38
|
end
|
36
39
|
|
@@ -12,6 +12,11 @@ module Transactable
|
|
12
12
|
include Dry::Monads[:result]
|
13
13
|
include Composable
|
14
14
|
|
15
|
+
def self.inherited descendant
|
16
|
+
super
|
17
|
+
descendant.prepend Instrumentable
|
18
|
+
end
|
19
|
+
|
15
20
|
def initialize *positionals, **keywords, &block
|
16
21
|
super(**keywords.slice(*DEPENDENCIES))
|
17
22
|
@base_positionals = positionals
|
data/lib/transactable.rb
CHANGED
data/transactable.gemspec
CHANGED
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.
|
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: 2023-
|
38
|
+
date: 2023-09-16 00:00:00.000000000 Z
|
39
39
|
dependencies:
|
40
40
|
- !ruby/object:Gem::Dependency
|
41
41
|
name: dry-container
|
@@ -152,6 +152,7 @@ files:
|
|
152
152
|
- lib/transactable/import.rb
|
153
153
|
- lib/transactable/instrument.rb
|
154
154
|
- lib/transactable/instrumentable.rb
|
155
|
+
- lib/transactable/pipe.rb
|
155
156
|
- lib/transactable/pipeable.rb
|
156
157
|
- lib/transactable/steps/abstract.rb
|
157
158
|
- lib/transactable/steps/as.rb
|
@@ -195,7 +196,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
195
196
|
- !ruby/object:Gem::Version
|
196
197
|
version: '0'
|
197
198
|
requirements: []
|
198
|
-
rubygems_version: 3.4.
|
199
|
+
rubygems_version: 3.4.19
|
199
200
|
signing_key:
|
200
201
|
specification_version: 4
|
201
202
|
summary: A domain specific language for functionally composable transactional workflows.
|
metadata.gz.sig
CHANGED
@@ -1,4 +1 @@
|
|
1
|
-
|
2
|
-
��d>�-�_y!�g��o��pP�:��.a1&�z���"eF7P���[ ��r��r�%d�/=��1>�P`Je����"��"�&w��nR?g� ��1���\�������~�;~��
|
3
|
-
*�'N?�_g�n��hsU�^�/ ��pҷ�;U��jP6h���C_4KS^'��n���9^V�����輇XC���y��u8���"`��ARx���0�Ջ�M��]vkU�~�{f
|
4
|
-
�,�m�*&�j�e2fͨI��4���v
|
1
|
+
}��6��,�������-Kش�aP�&d���Dž����x��)���`����[Y��#�r��y��k�s1r�ծ8�Y ��-�����_L� �0�Ԗ09�Z��m�G�ﳦ0p���q+�U���K(&l��&�GKi��Ph��e��r䞪��n`5��ޯb|�eY��ˑxP��K`g�Q��x~ΰ�Xu��*����G���7��P�תuql�C����_�}O�y;��B���g�h#��)���[.p3�_��������bEm���P��x0W��q5��0�.a������d;�qKfbe���$ !2��2���N�o�Mɷ'�8G1-�g��vB��'���#ؼR�"�NL�F�p*���]��?˘A��ڪ��
|