dry-transaction 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +6 -6
- data/README.md +11 -7
- data/lib/dry/transaction.rb +35 -43
- data/lib/dry/transaction/builder.rb +26 -0
- data/lib/dry/transaction/dsl.rb +30 -38
- data/lib/dry/transaction/instance_methods.rb +95 -0
- data/lib/dry/transaction/operation.rb +16 -0
- data/lib/dry/transaction/operation_resolver.rb +19 -0
- data/lib/dry/transaction/result_matcher.rb +2 -2
- data/lib/dry/transaction/step.rb +19 -8
- data/lib/dry/transaction/step_adapters.rb +1 -1
- data/lib/dry/transaction/step_adapters/map.rb +1 -1
- data/lib/dry/transaction/step_adapters/raw.rb +1 -1
- data/lib/dry/transaction/step_adapters/tee.rb +1 -1
- data/lib/dry/transaction/step_adapters/try.rb +1 -1
- data/lib/dry/transaction/step_failure.rb +1 -3
- data/lib/dry/transaction/version.rb +2 -2
- data/spec/examples.txt +60 -75
- data/spec/integration/custom_step_adapters_spec.rb +12 -12
- data/spec/integration/operation_spec.rb +30 -0
- data/spec/integration/passing_step_arguments_spec.rb +14 -13
- data/spec/integration/publishing_step_events_spec.rb +14 -12
- data/spec/integration/transaction_spec.rb +140 -22
- data/spec/spec_helper.rb +6 -2
- data/spec/unit/step_spec.rb +29 -0
- metadata +10 -10
- data/lib/dry/transaction/api.rb +0 -301
- data/lib/dry/transaction/step_definition.rb +0 -23
- data/spec/integration/custom_matcher_spec.rb +0 -59
- data/spec/unit/step_definition_spec.rb +0 -32
- data/spec/unit/transaction_spec.rb +0 -209
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 92a7c9469ad9306d832f60efe3bd036a9ddd9db6
|
|
4
|
+
data.tar.gz: 5ee2b52c8fabcc6d1e0e83ec7c1725a07bce23a7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bebec4fe3081d08e5d2c7942dd0d2ece14f6494419cb9c36ee7e8ea1e6c992bf6ffd4eba692248b51f68e9c0cb8072d9f3e7cb597145b1a95b33703e3f0820a8
|
|
7
|
+
data.tar.gz: f50379b4acf9cc3d54d8748cd8edcf149ed1f972529876ed8f19ac971ebb60e8d5e29b3922817009cdcbc4397788e0d37f970e3f01d5e296b9e00994f26febdc
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
dry-transaction (0.
|
|
4
|
+
dry-transaction (0.10.0)
|
|
5
5
|
dry-container (>= 0.2.8)
|
|
6
6
|
dry-matcher (>= 0.5.0)
|
|
7
7
|
dry-monads (>= 0.0.1)
|
|
@@ -15,10 +15,10 @@ GEM
|
|
|
15
15
|
codeclimate-test-reporter (0.5.0)
|
|
16
16
|
simplecov (>= 0.7.1, < 1.0.0)
|
|
17
17
|
coderay (1.1.1)
|
|
18
|
-
concurrent-ruby (1.0.
|
|
18
|
+
concurrent-ruby (1.0.5)
|
|
19
19
|
diff-lcs (1.2.5)
|
|
20
20
|
docile (1.1.5)
|
|
21
|
-
dry-configurable (0.
|
|
21
|
+
dry-configurable (0.7.0)
|
|
22
22
|
concurrent-ruby (~> 1.0)
|
|
23
23
|
dry-container (0.6.0)
|
|
24
24
|
concurrent-ruby (~> 1.0)
|
|
@@ -27,7 +27,7 @@ GEM
|
|
|
27
27
|
dry-matcher (0.6.0)
|
|
28
28
|
dry-monads (0.2.0)
|
|
29
29
|
dry-equalizer
|
|
30
|
-
json (1.8.
|
|
30
|
+
json (1.8.6)
|
|
31
31
|
method_source (0.8.2)
|
|
32
32
|
parser (2.3.1.4)
|
|
33
33
|
ast (~> 2.2)
|
|
@@ -72,7 +72,7 @@ PLATFORMS
|
|
|
72
72
|
ruby
|
|
73
73
|
|
|
74
74
|
DEPENDENCIES
|
|
75
|
-
bundler (~> 1.
|
|
75
|
+
bundler (~> 1.15)
|
|
76
76
|
byebug
|
|
77
77
|
codeclimate-test-reporter
|
|
78
78
|
dry-transaction!
|
|
@@ -84,4 +84,4 @@ DEPENDENCIES
|
|
|
84
84
|
yard
|
|
85
85
|
|
|
86
86
|
BUNDLED WITH
|
|
87
|
-
1.
|
|
87
|
+
1.15.1
|
data/README.md
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
[gitter]: https://gitter.im/dry-rb/chat
|
|
2
|
-
[gem]: https://
|
|
2
|
+
[gem]: https://img.shields.io/gem/v/dry-transaction.svg]
|
|
3
3
|
[travis]: https://travis-ci.org/dry-rb/dry-transaction
|
|
4
|
-
[
|
|
5
|
-
[
|
|
4
|
+
[gemnasium]: https://gemnasium.com/dry-rb/dry-transaction
|
|
5
|
+
[codeclimate]: https://codeclimate.com/github/dry-rb/dry-transaction
|
|
6
|
+
[coveralls]: https://coveralls.io/r/dry-rb/dry-transaction
|
|
7
|
+
[inchpages]: http://inch-ci.org/github/dry-rb/dry-transaction
|
|
6
8
|
|
|
7
|
-
# dry-transaction [](https://gitter.im/dry-rb/chat)
|
|
8
10
|
|
|
9
|
-
[][gem]
|
|
10
12
|
[][travis]
|
|
11
|
-
[][gemnasium]
|
|
14
|
+
[][codeclimate]
|
|
15
|
+
[][codeclimate]
|
|
16
|
+
[][inchpages]
|
|
13
17
|
|
|
14
18
|
dry-transaction is a business transaction DSL. It provides a simple way to define a complex business transaction that includes processing by many different objects.
|
|
15
19
|
|
data/lib/dry/transaction.rb
CHANGED
|
@@ -1,26 +1,35 @@
|
|
|
1
1
|
require "dry/monads/either"
|
|
2
2
|
require "dry/transaction/version"
|
|
3
|
-
require "dry/transaction/
|
|
4
|
-
require "dry/transaction/
|
|
3
|
+
require "dry/transaction/step_adapters"
|
|
4
|
+
require "dry/transaction/builder"
|
|
5
5
|
|
|
6
6
|
module Dry
|
|
7
|
-
#
|
|
7
|
+
# Business transaction DSL
|
|
8
|
+
module Transaction
|
|
9
|
+
def self.included(klass)
|
|
10
|
+
klass.send :include, Dry::Transaction()
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Build a module to make your class a business transaction.
|
|
8
15
|
#
|
|
9
16
|
# A business transaction is a series of callable operation objects that
|
|
10
17
|
# receive input and produce an output.
|
|
11
18
|
#
|
|
12
|
-
# The operations
|
|
13
|
-
# you pass when
|
|
14
|
-
# `#call(input, *args)`.
|
|
19
|
+
# The operations can be instance methods, or objects addressable via `#[]` in
|
|
20
|
+
# a container object that you pass when mixing in this module. The operations
|
|
21
|
+
# must respond to `#call(input, *args)`.
|
|
15
22
|
#
|
|
16
23
|
# Each operation will be called in the order it was specified in your
|
|
17
24
|
# transaction, with its output passed as the input to the next operation.
|
|
18
25
|
# Operations will only be called if the previous step was a success.
|
|
19
26
|
#
|
|
20
|
-
# A step is successful when it returns a [dry-monads](dry-monads) `Right`
|
|
21
|
-
# wrapping its output value. A step is a failure when it returns a
|
|
22
|
-
# object. If your operations already return a `Right` or `Left`, they
|
|
23
|
-
# added to your operation as plain `step` steps.
|
|
27
|
+
# A step is successful when it returns a [dry-monads](dry-monads) `Right`
|
|
28
|
+
# object wrapping its output value. A step is a failure when it returns a
|
|
29
|
+
# `Left` object. If your operations already return a `Right` or `Left`, they
|
|
30
|
+
# can be added to your operation as plain `step` steps.
|
|
31
|
+
#
|
|
32
|
+
# Add operation to your transaction with the `step` method.
|
|
24
33
|
#
|
|
25
34
|
# If your operations don't already return `Right` or `Left`, then they can be
|
|
26
35
|
# added to the transaction with the following steps:
|
|
@@ -34,46 +43,29 @@ module Dry
|
|
|
34
43
|
# [dry-monads]: https://rubygems.org/gems/dry-monads
|
|
35
44
|
#
|
|
36
45
|
# @example
|
|
37
|
-
#
|
|
46
|
+
# class MyTransaction
|
|
47
|
+
# include Dry::Transaction(container: MyContainer)
|
|
48
|
+
#
|
|
49
|
+
# step :first_step, with: "my_container.operations.first"
|
|
50
|
+
# step :second_step
|
|
38
51
|
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
52
|
+
# def second_step(input)
|
|
53
|
+
# result = do_something_with(input)
|
|
54
|
+
# Right(result)
|
|
55
|
+
# end
|
|
42
56
|
# end
|
|
43
57
|
#
|
|
58
|
+
# my_transaction = MyTransaction.new
|
|
44
59
|
# my_transaction.call(some_input)
|
|
45
60
|
#
|
|
46
|
-
# @param
|
|
47
|
-
# @
|
|
48
|
-
#
|
|
49
|
-
# @option options [Dry::Matcher] :matcher (Dry::Transaction::ResultMatcher) a custom matcher object for result matching block API
|
|
61
|
+
# @param container [#[]] the operations container
|
|
62
|
+
# @param step_adapters [#[]] (Dry::Transaction::StepAdapters) a
|
|
63
|
+
# custom container of step adapters
|
|
50
64
|
#
|
|
51
|
-
# @return [
|
|
65
|
+
# @return [Module] the transaction module
|
|
52
66
|
#
|
|
53
67
|
# @api public
|
|
54
|
-
def self.Transaction(
|
|
55
|
-
Transaction::
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# This is the class that actually stores the transaction.
|
|
59
|
-
# To be precise, it stores a series of steps that make up a transaction and
|
|
60
|
-
# a matcher for handling the result of the transaction.
|
|
61
|
-
#
|
|
62
|
-
# Never instantiate this class directly, it is intended to be created through
|
|
63
|
-
# the provided DSL.
|
|
64
|
-
class Transaction
|
|
65
|
-
include API
|
|
66
|
-
|
|
67
|
-
# @api private
|
|
68
|
-
attr_reader :steps
|
|
69
|
-
|
|
70
|
-
# @api private
|
|
71
|
-
attr_reader :matcher
|
|
72
|
-
|
|
73
|
-
# @api private
|
|
74
|
-
def initialize(steps, matcher)
|
|
75
|
-
@steps = steps
|
|
76
|
-
@matcher = matcher
|
|
77
|
-
end
|
|
68
|
+
def self.Transaction(container: nil, step_adapters: Transaction::StepAdapters)
|
|
69
|
+
Transaction::Builder.new(container: container, step_adapters: step_adapters)
|
|
78
70
|
end
|
|
79
71
|
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
require "dry/monads/either"
|
|
2
|
+
require "dry/transaction/step"
|
|
3
|
+
require "dry/transaction/dsl"
|
|
4
|
+
require "dry/transaction/instance_methods"
|
|
5
|
+
require "dry/transaction/operation_resolver"
|
|
6
|
+
|
|
7
|
+
module Dry
|
|
8
|
+
module Transaction
|
|
9
|
+
class Builder < Module
|
|
10
|
+
attr_reader :dsl_mod
|
|
11
|
+
attr_reader :resolver_mod
|
|
12
|
+
|
|
13
|
+
def initialize(container: nil, step_adapters:)
|
|
14
|
+
@dsl_mod = DSL.new(step_adapters: step_adapters)
|
|
15
|
+
@resolver_mod = OperationResolver.new(container)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def included(klass)
|
|
19
|
+
klass.extend(dsl_mod)
|
|
20
|
+
klass.send(:include, InstanceMethods)
|
|
21
|
+
klass.send(:prepend, resolver_mod)
|
|
22
|
+
klass.send(:include, Dry::Monads::Either::Mixin)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
data/lib/dry/transaction/dsl.rb
CHANGED
|
@@ -1,51 +1,43 @@
|
|
|
1
|
-
require "dry/transaction/result_matcher"
|
|
2
|
-
require "dry/transaction/step"
|
|
3
|
-
require "dry/transaction/step_adapters"
|
|
4
|
-
require "dry/transaction/step_definition"
|
|
5
|
-
|
|
6
1
|
module Dry
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
attr_reader :step_adapters
|
|
12
|
-
attr_reader :steps
|
|
13
|
-
attr_reader :matcher
|
|
14
|
-
|
|
15
|
-
def initialize(options, &block)
|
|
16
|
-
@container = options.fetch(:container)
|
|
17
|
-
@step_adapters = options.fetch(:step_adapters) { StepAdapters }
|
|
18
|
-
@steps = []
|
|
19
|
-
@matcher = options.fetch(:matcher) { ResultMatcher }
|
|
2
|
+
module Transaction
|
|
3
|
+
class DSL < Module
|
|
4
|
+
def initialize(step_adapters:)
|
|
5
|
+
@step_adapters = step_adapters
|
|
20
6
|
|
|
21
|
-
|
|
7
|
+
define_steps
|
|
8
|
+
define_dsl
|
|
22
9
|
end
|
|
23
10
|
|
|
24
|
-
def
|
|
25
|
-
step_adapters.
|
|
11
|
+
def inspect
|
|
12
|
+
"Dry::Transaction::DSL(#{@step_adapters.keys.sort.join(', ')})"
|
|
26
13
|
end
|
|
27
14
|
|
|
28
|
-
|
|
29
|
-
return super unless step_adapters.key?(method_name)
|
|
30
|
-
|
|
31
|
-
step_adapter = step_adapters[method_name]
|
|
32
|
-
step_name = args.first
|
|
33
|
-
options = args.last.is_a?(::Hash) ? args.last : {}
|
|
34
|
-
with = options.delete(:with)
|
|
15
|
+
private
|
|
35
16
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
operation = container[operation_name]
|
|
17
|
+
def define_steps
|
|
18
|
+
module_eval do
|
|
19
|
+
define_method(:steps) do
|
|
20
|
+
@steps ||= []
|
|
21
|
+
end
|
|
42
22
|
end
|
|
43
|
-
|
|
44
|
-
steps << Step.new(step_adapter, step_name, operation_name, operation, options, &block)
|
|
45
23
|
end
|
|
46
24
|
|
|
47
|
-
def
|
|
48
|
-
|
|
25
|
+
def define_dsl
|
|
26
|
+
module_exec(@step_adapters) do |step_adapters|
|
|
27
|
+
step_adapters.keys.each do |adapter_name|
|
|
28
|
+
define_method(adapter_name) do |step_name, with: nil, **options|
|
|
29
|
+
operation_name = with || step_name
|
|
30
|
+
|
|
31
|
+
steps << Step.new(
|
|
32
|
+
step_adapters[adapter_name],
|
|
33
|
+
step_name,
|
|
34
|
+
operation_name,
|
|
35
|
+
nil, # operations are resolved only when transactions are instantiated
|
|
36
|
+
options,
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
49
41
|
end
|
|
50
42
|
end
|
|
51
43
|
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
require "dry/monads"
|
|
2
|
+
require "dry/transaction/result_matcher"
|
|
3
|
+
|
|
4
|
+
module Dry
|
|
5
|
+
module Transaction
|
|
6
|
+
module InstanceMethods
|
|
7
|
+
attr_reader :steps
|
|
8
|
+
attr_reader :operations
|
|
9
|
+
|
|
10
|
+
def initialize(steps: (self.class.steps), **operations)
|
|
11
|
+
@steps = steps.map { |step|
|
|
12
|
+
operation = methods.include?(step.step_name) ? method(step.step_name) : operations[step.step_name]
|
|
13
|
+
step.with(operation: operation)
|
|
14
|
+
}
|
|
15
|
+
@operations = operations
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def call(input, &block)
|
|
19
|
+
assert_step_arity
|
|
20
|
+
|
|
21
|
+
result = steps.inject(Dry::Monads.Right(input), :bind)
|
|
22
|
+
|
|
23
|
+
if block
|
|
24
|
+
ResultMatcher.(result, &block)
|
|
25
|
+
else
|
|
26
|
+
result.or { |step_failure|
|
|
27
|
+
# Unwrap the value from the StepFailure and return it directly
|
|
28
|
+
Dry::Monads.Left(step_failure.value)
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def subscribe(listeners)
|
|
34
|
+
if listeners.is_a?(Hash)
|
|
35
|
+
listeners.each do |step_name, listener|
|
|
36
|
+
steps.detect { |step| step.step_name == step_name }.subscribe(listener)
|
|
37
|
+
end
|
|
38
|
+
else
|
|
39
|
+
steps.each do |step|
|
|
40
|
+
step.subscribe(listeners)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def with_step_args(**step_args)
|
|
46
|
+
assert_valid_step_args(step_args)
|
|
47
|
+
|
|
48
|
+
new_steps = steps.map { |step|
|
|
49
|
+
if step_args[step.step_name]
|
|
50
|
+
step.with(call_args: step_args[step.step_name])
|
|
51
|
+
else
|
|
52
|
+
step
|
|
53
|
+
end
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
self.class.new(steps: new_steps, **operations)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def respond_to_missing?(name, _include_private = false)
|
|
62
|
+
steps.any? { |step| step.step_name == name }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def method_missing(name, *args, &block)
|
|
66
|
+
step = steps.detect { |s| s.step_name == name }
|
|
67
|
+
super unless step
|
|
68
|
+
|
|
69
|
+
operation = operations[step.step_name]
|
|
70
|
+
raise NotImplementedError, "no operation +#{step.operation_name}+ defined for step +#{step.step_name}+" unless operation
|
|
71
|
+
|
|
72
|
+
operation.(*args, &block)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def assert_valid_step_args(step_args)
|
|
76
|
+
step_args.each_key do |step_name|
|
|
77
|
+
unless steps.any? { |step| step.step_name == step_name }
|
|
78
|
+
raise ArgumentError, "+#{step_name}+ is not a valid step name"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def assert_step_arity
|
|
84
|
+
steps.each do |step|
|
|
85
|
+
num_args_required = step.arity >= 0 ? step.arity : ~step.arity
|
|
86
|
+
num_args_supplied = step.call_args.length + 1 # add 1 for main `input`
|
|
87
|
+
|
|
88
|
+
if num_args_required > num_args_supplied
|
|
89
|
+
raise ArgumentError, "not enough arguments supplied for step +#{step.step_name}+"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
require "dry/monads/either"
|
|
2
|
+
require "dry/matcher"
|
|
3
|
+
require "dry/matcher/either_matcher"
|
|
4
|
+
|
|
5
|
+
module Dry
|
|
6
|
+
module Transaction
|
|
7
|
+
module Operation
|
|
8
|
+
def self.included(klass)
|
|
9
|
+
klass.class_eval do
|
|
10
|
+
include Dry::Monads::Either::Mixin
|
|
11
|
+
include Dry::Matcher.for(:call, with: Dry::Matcher::EitherMatcher)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Dry
|
|
2
|
+
module Transaction
|
|
3
|
+
class OperationResolver < Module
|
|
4
|
+
def initialize(container)
|
|
5
|
+
module_exec(container) do |ops_container|
|
|
6
|
+
define_method :initialize do |**kwargs|
|
|
7
|
+
operation_kwargs = self.class.steps.select(&:operation_name).map { |step|
|
|
8
|
+
operation = kwargs.fetch(step.step_name) { ops_container and ops_container[step.operation_name] }
|
|
9
|
+
|
|
10
|
+
[step.step_name, operation]
|
|
11
|
+
}.to_h
|
|
12
|
+
|
|
13
|
+
super(**kwargs, **operation_kwargs)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|