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 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? },