transactable 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/monads/result"
4
+
5
+ module Transactable
6
+ # Allows any object to pipe sequential steps together which can be composed into a single result.
7
+ class Pipeable < Module
8
+ def initialize steps = Steps::Container
9
+ super()
10
+ @steps = steps
11
+ @instance_module = Class.new(Module).new
12
+ end
13
+
14
+ def included klass
15
+ super
16
+ define_pipe
17
+ define_steps
18
+ klass.include instance_module
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :steps, :instance_module
24
+
25
+ def define_pipe
26
+ instance_module.define_method :pipe do |input, *steps|
27
+ fail ArgumentError, "Transaction must have at least one step." if steps.empty?
28
+
29
+ result = input.is_a?(Dry::Monads::Result) ? input : Dry::Monads::Success(input)
30
+
31
+ steps.reduce(&:>>).call result
32
+ rescue NoMethodError
33
+ raise TypeError, "Step must be functionally composable and answer a monad."
34
+ end
35
+ end
36
+
37
+ def define_steps
38
+ instance_module.class_exec steps do |container|
39
+ container.each_key do |name|
40
+ define_method name do |*positionals, **keywords, &block|
41
+ step = container[name]
42
+ step.is_a?(Proc) ? step : step.new(*positionals, **keywords, &block)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/monads"
4
+
5
+ module Transactable
6
+ module Steps
7
+ # Provides the blueprint for a step to used in function composition.
8
+ class Abstract
9
+ DEPENDENCIES = %i[instrument marameters].freeze
10
+
11
+ # rubocop:todo Layout/ClassStructure
12
+ include Import[*DEPENDENCIES]
13
+ # rubocop:enable Layout/ClassStructure
14
+ include Dry::Monads[:result]
15
+ include Composable
16
+
17
+ def initialize *positionals, **keywords, &block
18
+ super(**keywords.slice(*DEPENDENCIES))
19
+ @base_positionals = positionals
20
+ @base_keywords = keywords.except(*DEPENDENCIES)
21
+ @base_block = block
22
+ end
23
+
24
+ protected
25
+
26
+ attr_reader :base_positionals, :base_keywords, :base_block
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Transactable
4
+ module Steps
5
+ # Allows result to be messaged as a callable.
6
+ class As < Abstract
7
+ prepend Instrumentable
8
+
9
+ def call result
10
+ result.fmap { |operation| operation.public_send(*base_positionals, **base_keywords) }
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Transactable
4
+ module Steps
5
+ # Wraps Dry Monads `#bind` method as a step.
6
+ class Bind < Abstract
7
+ prepend Instrumentable
8
+
9
+ def call(result) = result.bind { |input| base_block.call input }
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Transactable
4
+ module Steps
5
+ # Checks if operation is true and then answers success (passthrough) or failure (with argument).
6
+ class Check < Abstract
7
+ prepend Instrumentable
8
+
9
+ def initialize operation, message, **dependencies
10
+ super(**dependencies)
11
+ @operation = operation
12
+ @message = message
13
+ end
14
+
15
+ def call result
16
+ result.bind do |arguments|
17
+ answer = question arguments
18
+ answer == true || answer.is_a?(Success) ? result : Failure(arguments)
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :operation, :message
25
+
26
+ def question arguments
27
+ splat = marameters.categorize operation.method(message).parameters, arguments
28
+ operation.public_send(message, *splat.positionals, **splat.keywords, &splat.block)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/container"
4
+
5
+ module Transactable
6
+ module Steps
7
+ # Registers all default steps.
8
+ module Container
9
+ extend Dry::Container::Mixin
10
+
11
+ register(:as) { As }
12
+ register(:bind) { Bind }
13
+ register(:check) { Check }
14
+ register(:fmap) { Fmap }
15
+ register(:insert) { Insert }
16
+ register(:map) { Map }
17
+ register(:merge) { Merge }
18
+ register(:orr) { Or }
19
+ register(:tee) { Tee }
20
+ register(:to) { To }
21
+ register(:try) { Try }
22
+ register(:use) { Use }
23
+ register(:validate) { Validate }
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Transactable
4
+ module Steps
5
+ # Wraps Dry Monads `#fmap` method as a step.
6
+ class Fmap < Abstract
7
+ prepend Instrumentable
8
+
9
+ def call(result) = result.fmap { |input| base_block.call input }
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Transactable
4
+ module Steps
5
+ # Inserts elements before, after, or around input.
6
+ class Insert < Abstract
7
+ prepend Instrumentable
8
+
9
+ LAST = -1
10
+
11
+ def initialize *positionals, at: LAST, **dependencies
12
+ super(*positionals, **dependencies)
13
+ @value = positionals.empty? ? base_keywords : positionals.flatten
14
+ @at = at
15
+ end
16
+
17
+ def call result
18
+ result.fmap do |input|
19
+ cast = input.is_a?(Array) ? input : [input]
20
+ value.is_a?(Array) ? cast.insert(at, *value) : cast.insert(at, value)
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :value, :at
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Transactable
4
+ module Steps
5
+ # Maps over a collection, processing each element, and answering a new result.
6
+ class Map < Abstract
7
+ prepend Instrumentable
8
+
9
+ def call(result) = result.fmap { |collection| collection.map(&base_block) }
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Transactable
4
+ module Steps
5
+ # Merges initialized attributes with step argument for use by a subsequent step.
6
+ class Merge < Abstract
7
+ prepend Instrumentable
8
+
9
+ def initialize as: :step, **keywords
10
+ super(**keywords)
11
+ @as = as
12
+ end
13
+
14
+ def call result
15
+ result.fmap do |input|
16
+ if input.is_a? Hash
17
+ input.merge! base_keywords
18
+ else
19
+ {as => input}.merge!(base_keywords)
20
+ end
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :as
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Transactable
4
+ module Steps
5
+ # Wraps Dry Monads `#or` method as a step.
6
+ class Or < Abstract
7
+ prepend Instrumentable
8
+
9
+ def call(result) = result.or { |input| base_block.call input }
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Transactable
4
+ module Steps
5
+ # Messages operation, without any response checks, while passing input through as output.
6
+ class Tee < Abstract
7
+ prepend Instrumentable
8
+
9
+ def initialize operation, *positionals, **dependencies
10
+ super(*positionals, **dependencies)
11
+ @operation = operation
12
+ end
13
+
14
+ def call result
15
+ operation.public_send(*base_positionals, **base_keywords)
16
+ result
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :operation
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Transactable
4
+ module Steps
5
+ # Delegates to a non-callable operation which automatically wraps the result if necessary.
6
+ class To < Abstract
7
+ prepend Instrumentable
8
+
9
+ def initialize operation, message, **dependencies
10
+ super(**dependencies)
11
+ @operation = operation
12
+ @message = message
13
+ end
14
+
15
+ def call result
16
+ result.bind do |arguments|
17
+ splat = marameters.categorize operation.method(message).parameters, arguments
18
+ wrap operation.public_send(message, *splat.positionals, **splat.keywords, &splat.block)
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :operation, :message
25
+
26
+ def wrap(result) = result.is_a?(Dry::Monads::Result) ? result : Success(result)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Transactable
4
+ module Steps
5
+ # Messages a risky operation which may pass or fail.
6
+ class Try < Abstract
7
+ prepend Instrumentable
8
+
9
+ def initialize *positionals, catch:, **keywords
10
+ super(*positionals, **keywords)
11
+ @catch = catch
12
+ end
13
+
14
+ def call result
15
+ result.fmap { |operation| operation.public_send(*base_positionals, **base_keywords) }
16
+ rescue *Array(catch) => error
17
+ Failure error.message
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :catch
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Transactable
4
+ module Steps
5
+ # Use another transaction -- or any command -- which answers a result.
6
+ class Use < Abstract
7
+ prepend Instrumentable
8
+
9
+ def initialize operation, **dependencies
10
+ super(**dependencies)
11
+ @operation = operation
12
+ end
13
+
14
+ def call(result) = result.bind { |input| operation.call input }
15
+
16
+ private
17
+
18
+ attr_reader :operation
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Transactable
4
+ module Steps
5
+ # Validates a result via a callable operation.
6
+ class Validate < Abstract
7
+ prepend Instrumentable
8
+
9
+ def initialize operation, as: :to_h, **dependencies
10
+ super(**dependencies)
11
+ @operation = operation
12
+ @as = as
13
+ end
14
+
15
+ def call result
16
+ result.bind do |payload|
17
+ value = operation.call payload
18
+
19
+ return Failure value if value.failure?
20
+
21
+ Success(as ? value.public_send(as) : value)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :operation, :as
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zeitwerk"
4
+
5
+ Zeitwerk::Loader.for_gem.setup
6
+
7
+ # Main namespace.
8
+ module Transactable
9
+ def self.included(klass) = klass.include Pipeable.new
10
+
11
+ def self.with(...) = Pipeable.new(...)
12
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "transactable"
5
+ spec.version = "0.0.0"
6
+ spec.authors = ["Brooke Kuhlmann"]
7
+ spec.email = ["brooke@alchemists.io"]
8
+ spec.homepage = "https://www.alchemists.io/projects/transactable"
9
+ spec.summary = "A DSL for transactional workflows built atop function composition."
10
+ spec.license = "Hippocratic-2.1"
11
+
12
+ spec.metadata = {
13
+ "bug_tracker_uri" => "https://github.com/bkuhlmann/transactable/issues",
14
+ "changelog_uri" => "https://www.alchemists.io/projects/transactable/versions",
15
+ "documentation_uri" => "https://www.alchemists.io/projects/transactable",
16
+ "funding_uri" => "https://github.com/sponsors/bkuhlmann",
17
+ "label" => "Transactable",
18
+ "rubygems_mfa_required" => "true",
19
+ "source_code_uri" => "https://github.com/bkuhlmann/transactable"
20
+ }
21
+
22
+ spec.signing_key = Gem.default_key_path
23
+ spec.cert_chain = [Gem.default_cert_path]
24
+
25
+ spec.required_ruby_version = "~> 3.1"
26
+
27
+ spec.add_dependency "dry-container", "~> 0.10"
28
+ spec.add_dependency "dry-events", "~> 0.3"
29
+ spec.add_dependency "dry-monads", "~> 1.4"
30
+ spec.add_dependency "infusible", "~> 0.0"
31
+ spec.add_dependency "marameters", "~> 0.9"
32
+ spec.add_dependency "refinements", "~> 9.6"
33
+ spec.add_dependency "zeitwerk", "~> 2.6"
34
+
35
+ spec.extra_rdoc_files = Dir["README*", "LICENSE*"]
36
+ spec.files = Dir["*.gemspec", "lib/**/*"]
37
+ end
data.tar.gz.sig ADDED
Binary file
metadata ADDED
@@ -0,0 +1,195 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: transactable
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Brooke Kuhlmann
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain:
11
+ - |
12
+ -----BEGIN CERTIFICATE-----
13
+ MIIC/jCCAeagAwIBAgIBBTANBgkqhkiG9w0BAQsFADAlMSMwIQYDVQQDDBpicm9v
14
+ a2UvREM9YWxjaGVtaXN0cy9EQz1pbzAeFw0yMjAzMTkxNzI0MzJaFw0yMzAzMTkx
15
+ NzI0MzJaMCUxIzAhBgNVBAMMGmJyb29rZS9EQz1hbGNoZW1pc3RzL0RDPWlvMIIB
16
+ IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6l1qpXTiomH1RfMRloyw7MiE
17
+ xyVx/x8Yc3EupdH7uhNaTXQGyORN6aOY//1QXXMHIZ9tW74nZLhesWMSUMYy0XhB
18
+ brs+KkurHnc9FnEJAbG7ebGvl/ncqZt72nQvaxpDxvuCBHgJAz+8i5wl6FhLw+oT
19
+ 9z0A8KcGhz67SdcoQiD7qiCjL/2NTeWHOzkpPrdGlt088+VerEEGf5I13QCvaftP
20
+ D5vkU0YlAm1r98BymuJlcQ1qdkVEI1d48ph4kcS0S0nv1RiuyVb6TCAR3Nu3VaVq
21
+ 3fPzZKJLZBx67UvXdbdicWPiUR75elI4PXpLIic3xytaF52ZJYyKZCNZJhNwfQID
22
+ AQABozkwNzAJBgNVHRMEAjAAMAsGA1UdDwQEAwIEsDAdBgNVHQ4EFgQU0nzow9vc
23
+ 2CdikiiE3fJhP/gY4ggwDQYJKoZIhvcNAQELBQADggEBAJbbNyWzFjqUNVPPCUCo
24
+ IMrhDa9xf1xkORXNYYbmXgoxRy/KyNbUr+jgEEoWJAm9GXlcqxxWAUI6pK/i4/Qi
25
+ X6rPFEFmeObDOHNvuqy8Hd6AYsu+kP94U/KJhe9wnWGMmGoNKJNU3EkW3jM/osSl
26
+ +JRxiH5t4WtnDiVyoYl5nYC02rYdjJkG6VMxDymXTqn7u6HhYgZkGujq1UPar8x2
27
+ hNIWJblDKKSu7hA2d6+kUthuYo13o1sg1Da/AEDg0hoZSUvhqDEF5Hy232qb3pDt
28
+ CxDe2+VuChj4I1nvIHdu+E6XoEVlanUPKmSg6nddhkKn2gC45Kyzh6FZqnzH/CRp
29
+ RFE=
30
+ -----END CERTIFICATE-----
31
+ date: 2022-09-10 00:00:00.000000000 Z
32
+ dependencies:
33
+ - !ruby/object:Gem::Dependency
34
+ name: dry-container
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.10'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0.10'
47
+ - !ruby/object:Gem::Dependency
48
+ name: dry-events
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '0.3'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '0.3'
61
+ - !ruby/object:Gem::Dependency
62
+ name: dry-monads
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.4'
68
+ type: :runtime
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '1.4'
75
+ - !ruby/object:Gem::Dependency
76
+ name: infusible
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '0.0'
82
+ type: :runtime
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '0.0'
89
+ - !ruby/object:Gem::Dependency
90
+ name: marameters
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '0.9'
96
+ type: :runtime
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '0.9'
103
+ - !ruby/object:Gem::Dependency
104
+ name: refinements
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '9.6'
110
+ type: :runtime
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '9.6'
117
+ - !ruby/object:Gem::Dependency
118
+ name: zeitwerk
119
+ requirement: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '2.6'
124
+ type: :runtime
125
+ prerelease: false
126
+ version_requirements: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: '2.6'
131
+ description:
132
+ email:
133
+ - brooke@alchemists.io
134
+ executables: []
135
+ extensions: []
136
+ extra_rdoc_files:
137
+ - README.adoc
138
+ - LICENSE.adoc
139
+ files:
140
+ - LICENSE.adoc
141
+ - README.adoc
142
+ - lib/transactable.rb
143
+ - lib/transactable/composable.rb
144
+ - lib/transactable/container.rb
145
+ - lib/transactable/import.rb
146
+ - lib/transactable/instrument.rb
147
+ - lib/transactable/instrumentable.rb
148
+ - lib/transactable/pipeable.rb
149
+ - lib/transactable/steps/abstract.rb
150
+ - lib/transactable/steps/as.rb
151
+ - lib/transactable/steps/bind.rb
152
+ - lib/transactable/steps/check.rb
153
+ - lib/transactable/steps/container.rb
154
+ - lib/transactable/steps/fmap.rb
155
+ - lib/transactable/steps/insert.rb
156
+ - lib/transactable/steps/map.rb
157
+ - lib/transactable/steps/merge.rb
158
+ - lib/transactable/steps/or.rb
159
+ - lib/transactable/steps/tee.rb
160
+ - lib/transactable/steps/to.rb
161
+ - lib/transactable/steps/try.rb
162
+ - lib/transactable/steps/use.rb
163
+ - lib/transactable/steps/validate.rb
164
+ - transactable.gemspec
165
+ homepage: https://www.alchemists.io/projects/transactable
166
+ licenses:
167
+ - Hippocratic-2.1
168
+ metadata:
169
+ bug_tracker_uri: https://github.com/bkuhlmann/transactable/issues
170
+ changelog_uri: https://www.alchemists.io/projects/transactable/versions
171
+ documentation_uri: https://www.alchemists.io/projects/transactable
172
+ funding_uri: https://github.com/sponsors/bkuhlmann
173
+ label: Transactable
174
+ rubygems_mfa_required: 'true'
175
+ source_code_uri: https://github.com/bkuhlmann/transactable
176
+ post_install_message:
177
+ rdoc_options: []
178
+ require_paths:
179
+ - lib
180
+ required_ruby_version: !ruby/object:Gem::Requirement
181
+ requirements:
182
+ - - "~>"
183
+ - !ruby/object:Gem::Version
184
+ version: '3.1'
185
+ required_rubygems_version: !ruby/object:Gem::Requirement
186
+ requirements:
187
+ - - ">="
188
+ - !ruby/object:Gem::Version
189
+ version: '0'
190
+ requirements: []
191
+ rubygems_version: 3.3.22
192
+ signing_key:
193
+ specification_version: 4
194
+ summary: A DSL for transactional workflows built atop function composition.
195
+ test_files: []
metadata.gz.sig ADDED
Binary file