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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 19a5598d0c611fa9cd909c25cd76fccee443f925
4
- data.tar.gz: f7dd7277b815393f51b7eb8cac00db2d8011ab83
3
+ metadata.gz: 92a7c9469ad9306d832f60efe3bd036a9ddd9db6
4
+ data.tar.gz: 5ee2b52c8fabcc6d1e0e83ec7c1725a07bce23a7
5
5
  SHA512:
6
- metadata.gz: 3310c5583f9fc9c3d84dad54db9b71908227309e4325adc9f38d3a26aca64a699a66e75d0a3d05173beaf002d246fea7ed434300793ad1308bd6638fbb404505
7
- data.tar.gz: 6dea0341d58c8da07a06bb1ec2fbb82ca793a47969f6f4acacf29dd445996910fdbcc9f096f0d286b4aabcef6343a946262f3b98197155059010294e3f9eb448
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.9.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.2)
18
+ concurrent-ruby (1.0.5)
19
19
  diff-lcs (1.2.5)
20
20
  docile (1.1.5)
21
- dry-configurable (0.4.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.3)
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.13.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.13.2
87
+ 1.15.1
data/README.md CHANGED
@@ -1,15 +1,19 @@
1
1
  [gitter]: https://gitter.im/dry-rb/chat
2
- [gem]: https://rubygems.org/gems/dry-transaction
2
+ [gem]: https://img.shields.io/gem/v/dry-transaction.svg]
3
3
  [travis]: https://travis-ci.org/dry-rb/dry-transaction
4
- [code_climate]: https://codeclimate.com/github/dry-rb/dry-transaction
5
- [inch]: http://inch-ci.org/github/dry-rb/dry-transaction
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 Gitter chat](https://badges.gitter.im/Join%20Chat.svg)][gitter]
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://img.shields.io/gem/v/dry-transaction.svg)][gem]
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
- [![Code Climate](https://img.shields.io/codeclimate/github/dry-rb/dry-transaction.svg)][code_climate]
12
- [![API Documentation Coverage](http://inch-ci.org/github/dry-rb/dry-transaction.svg)][inch]
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
 
@@ -1,26 +1,35 @@
1
1
  require "dry/monads/either"
2
2
  require "dry/transaction/version"
3
- require "dry/transaction/dsl"
4
- require "dry/transaction/api"
3
+ require "dry/transaction/step_adapters"
4
+ require "dry/transaction/builder"
5
5
 
6
6
  module Dry
7
- # Define a business transaction.
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 should be addressable via `#[]` in a container object that
13
- # you pass when creating the transaction. The operations must respond to
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` object
21
- # wrapping its output value. A step is a failure when it returns a `Left`
22
- # object. If your operations already return a `Right` or `Left`, they can be
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
- # container = {do_first: some_obj, do_second: some_obj}
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
- # my_transaction = Dry.Transaction(container: container) do
40
- # step :do_first
41
- # step :do_second
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 options [Hash] the options hash
47
- # @option options [#[]] :container the operations container
48
- # @option options [#[]] :step_adapters (Dry::Transaction::StepAdapters) a custom container of step adapters
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 [Dry::Transaction] the transaction object
65
+ # @return [Module] the transaction module
52
66
  #
53
67
  # @api public
54
- def self.Transaction(options = {}, &block)
55
- Transaction::DSL.new(options, &block).call
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
@@ -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
- class Transaction
8
- # @api private
9
- class DSL < BasicObject
10
- attr_reader :container
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
- instance_eval(&block)
7
+ define_steps
8
+ define_dsl
22
9
  end
23
10
 
24
- def respond_to_missing?(method_name)
25
- step_adapters.key?(method_name)
11
+ def inspect
12
+ "Dry::Transaction::DSL(#{@step_adapters.keys.sort.join(', ')})"
26
13
  end
27
14
 
28
- def method_missing(method_name, *args, &block)
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
- if with.respond_to?(:call)
37
- operation_name = step_name
38
- operation = StepDefinition.new(container, &with)
39
- else
40
- operation_name = with || step_name
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 call
48
- Transaction.new(steps, matcher)
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
@@ -1,7 +1,7 @@
1
- require "dry-matcher"
1
+ require "dry/matcher"
2
2
 
3
3
  module Dry
4
- class Transaction
4
+ module Transaction
5
5
  ResultMatcher = Dry::Matcher.new(
6
6
  success: Dry::Matcher::Case.new(
7
7
  match: -> result { result.right? },