dry-transaction 0.9.0 → 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 [![Join the
|
9
|
+
# dry-transaction [![Join the chat at https://gitter.im/dry-rb/chat](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/dry-rb/chat)
|
8
10
|
|
9
|
-
[![Gem Version](https://
|
11
|
+
[![Gem Version](https://badge.fury.io/rb/dry-transaction.svg)][gem]
|
10
12
|
[![Build Status](https://travis-ci.org/dry-rb/dry-transaction.svg?branch=master)][travis]
|
11
|
-
[![
|
12
|
-
[![
|
13
|
+
[![Dependency Status](https://gemnasium.com/dry-rb/dry-transaction.svg)][gemnasium]
|
14
|
+
[![Code Climate](https://codeclimate.com/github/dry-rb/dry-transaction/badges/gpa.svg)][codeclimate]
|
15
|
+
[![Test Coverage](https://codeclimate.com/github/dry-rb/dry-transaction/badges/coverage.svg)][codeclimate]
|
16
|
+
[![Inline docs](http://inch-ci.org/github/dry-rb/dry-transaction.svg?branch=master)][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
|