dry-transaction 0.11.2 → 0.13.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +317 -0
  3. data/LICENSE +20 -0
  4. data/README.md +15 -43
  5. data/dry-transaction.gemspec +35 -0
  6. data/lib/dry-transaction.rb +2 -0
  7. data/lib/dry/transaction.rb +2 -0
  8. data/lib/dry/transaction/builder.rb +6 -4
  9. data/lib/dry/transaction/callable.rb +18 -0
  10. data/lib/dry/transaction/dsl.rb +9 -7
  11. data/lib/dry/transaction/errors.rb +2 -0
  12. data/lib/dry/transaction/instance_methods.rb +24 -20
  13. data/lib/dry/transaction/operation.rb +5 -3
  14. data/lib/dry/transaction/operation_resolver.rb +4 -2
  15. data/lib/dry/transaction/result_matcher.rb +7 -5
  16. data/lib/dry/transaction/stack.rb +2 -0
  17. data/lib/dry/transaction/step.rb +37 -24
  18. data/lib/dry/transaction/step_adapter.rb +6 -4
  19. data/lib/dry/transaction/step_adapters.rb +9 -7
  20. data/lib/dry/transaction/step_adapters/around.rb +4 -2
  21. data/lib/dry/transaction/step_adapters/check.rb +2 -0
  22. data/lib/dry/transaction/step_adapters/map.rb +2 -0
  23. data/lib/dry/transaction/step_adapters/raw.rb +5 -3
  24. data/lib/dry/transaction/step_adapters/tee.rb +2 -0
  25. data/lib/dry/transaction/step_adapters/try.rb +3 -1
  26. data/lib/dry/transaction/step_failure.rb +12 -0
  27. data/lib/dry/transaction/version.rb +3 -1
  28. metadata +14 -110
  29. data/Gemfile +0 -15
  30. data/Gemfile.lock +0 -97
  31. data/LICENSE.md +0 -9
  32. data/Rakefile +0 -6
  33. data/spec/examples.txt +0 -83
  34. data/spec/integration/around_spec.rb +0 -81
  35. data/spec/integration/auto_injection_spec.rb +0 -32
  36. data/spec/integration/custom_step_adapters_spec.rb +0 -41
  37. data/spec/integration/operation_spec.rb +0 -30
  38. data/spec/integration/passing_step_arguments_spec.rb +0 -51
  39. data/spec/integration/publishing_step_events_spec.rb +0 -119
  40. data/spec/integration/transaction_spec.rb +0 -566
  41. data/spec/integration/transaction_without_steps_spec.rb +0 -101
  42. data/spec/spec_helper.rb +0 -116
  43. data/spec/support/container.rb +0 -10
  44. data/spec/support/database.rb +0 -12
  45. data/spec/support/db_transactions.rb +0 -45
  46. data/spec/support/result_mixin.rb +0 -3
  47. data/spec/support/test_module_constants.rb +0 -11
  48. data/spec/unit/step_adapters/around_spec.rb +0 -46
  49. data/spec/unit/step_adapters/check_spec.rb +0 -43
  50. data/spec/unit/step_adapters/map_spec.rb +0 -16
  51. data/spec/unit/step_adapters/raw_spec.rb +0 -36
  52. data/spec/unit/step_adapters/tee_spec.rb +0 -17
  53. data/spec/unit/step_adapters/try_spec.rb +0 -89
  54. data/spec/unit/step_spec.rb +0 -131
@@ -1 +1,3 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "dry/transaction"
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "dry/monads/result"
2
4
  require "dry/transaction/version"
3
5
  require "dry/transaction/step_adapters"
@@ -1,7 +1,9 @@
1
- require "dry/transaction/step"
2
- require "dry/transaction/dsl"
3
- require "dry/transaction/instance_methods"
4
- require "dry/transaction/operation_resolver"
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/transaction/step'
4
+ require 'dry/transaction/dsl'
5
+ require 'dry/transaction/instance_methods'
6
+ require 'dry/transaction/operation_resolver'
5
7
 
6
8
  module Dry
7
9
  module Transaction
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Dry
2
4
  module Transaction
3
5
  # @api private
@@ -29,10 +31,26 @@ module Dry
29
31
  def call(*args, &block)
30
32
  if arity.zero?
31
33
  operation.(&block)
34
+ elsif ruby_27_last_arg_hash?(args)
35
+ *prefix, last = args
36
+ operation.(*prefix, **last, &block)
32
37
  else
33
38
  operation.(*args, &block)
34
39
  end
35
40
  end
41
+
42
+ private
43
+
44
+ # Ruby 2.7 gives a deprecation warning about passing a hash of parameters as the last argument
45
+ # to a method. Ruby 3.0 outright disallows it. This checks for that condition, but explicitly
46
+ # uses instance_of? rather than is_a? or kind_of?, because Hash like objects, specifically
47
+ # HashWithIndifferentAccess objects are provided by Rails as controller parameters, and often
48
+ # passed to dry-rb validators.
49
+ # In this case, it's better to leave the object as it's existing type, rather than implicitly
50
+ # convert it in to a hash with the double-splat (**) operator.
51
+ def ruby_27_last_arg_hash?(args)
52
+ args.last.instance_of?(Hash) && Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.7.0")
53
+ end
36
54
  end
37
55
  end
38
56
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Dry
2
4
  module Transaction
3
5
  class DSL < Module
@@ -24,16 +26,16 @@ module Dry
24
26
 
25
27
  def define_dsl
26
28
  module_exec(@step_adapters) do |step_adapters|
27
- step_adapters.keys.each do |adapter_name|
29
+ step_adapters.each do |adapter_name, adapter|
28
30
  define_method(adapter_name) do |step_name, with: nil, **options|
29
- operation_name = with || step_name
31
+ operation_name = with
30
32
 
31
33
  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,
34
+ adapter: adapter,
35
+ name: step_name,
36
+ operation_name: operation_name,
37
+ operation: nil, # operations are resolved only when transactions are instantiated
38
+ options: options,
37
39
  )
38
40
  end
39
41
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Dry
2
4
  module Transaction
3
5
  class InvalidStepError < ArgumentError
@@ -1,6 +1,8 @@
1
- require "dry/monads/result"
2
- require "dry/transaction/result_matcher"
3
- require "dry/transaction/stack"
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/monads/result'
4
+ require 'dry/transaction/result_matcher'
5
+ require 'dry/transaction/stack'
4
6
 
5
7
  module Dry
6
8
  module Transaction
@@ -14,7 +16,7 @@ module Dry
14
16
 
15
17
  def initialize(steps: (self.class.steps), listeners: nil, **operations)
16
18
  @steps = steps.map { |step|
17
- operation = resolve_operation(step, operations)
19
+ operation = resolve_operation(step, **operations)
18
20
  step.with(operation: operation)
19
21
  }
20
22
  @operations = operations
@@ -42,7 +44,7 @@ module Dry
42
44
 
43
45
  if listeners.is_a?(Hash)
44
46
  listeners.each do |step_name, listener|
45
- steps.detect { |step| step.step_name == step_name }.subscribe(listener)
47
+ steps.detect { |step| step.name == step_name }.subscribe(listener)
46
48
  end
47
49
  else
48
50
  steps.each do |step|
@@ -55,8 +57,8 @@ module Dry
55
57
  assert_valid_step_args(step_args)
56
58
 
57
59
  new_steps = steps.map { |step|
58
- if step_args[step.step_name]
59
- step.with(call_args: step_args[step.step_name])
60
+ if step_args[step.name]
61
+ step.with(call_args: step_args[step.name])
60
62
  else
61
63
  step
62
64
  end
@@ -68,34 +70,36 @@ module Dry
68
70
  private
69
71
 
70
72
  def respond_to_missing?(name, _include_private = false)
71
- steps.any? { |step| step.step_name == name }
73
+ steps.any? { |step| step.name == name }
72
74
  end
73
75
 
74
76
  def method_missing(name, *args, &block)
75
- step = steps.detect { |s| s.step_name == name }
77
+ step = steps.detect { |s| s.name == name }
76
78
  super unless step
77
79
 
78
- operation = operations[step.step_name]
79
- raise NotImplementedError, "no operation +#{step.operation_name}+ defined for step +#{step.step_name}+" unless operation
80
+ operation = operations[step.name]
81
+ raise NotImplementedError, "no operation +#{step.operation_name}+ defined for step +#{step.name}+" unless operation
80
82
 
81
83
  operation.(*args, &block)
82
84
  end
83
85
 
84
86
  def resolve_operation(step, **operations)
85
- if methods.include?(step.step_name) || private_methods.include?(step.step_name)
86
- method(step.step_name)
87
- elsif operations[step.step_name].nil?
88
- raise MissingStepError.new(step.step_name)
89
- elsif operations[step.step_name].respond_to?(:call)
90
- operations[step.step_name]
87
+ if step.internal? && operations[step.name]
88
+ operations[step.name]
89
+ elsif methods.include?(step.name) || private_methods.include?(step.name)
90
+ method(step.name)
91
+ elsif operations[step.name].respond_to?(:call)
92
+ operations[step.name]
93
+ elsif operations[step.name]
94
+ raise InvalidStepError.new(step.name)
91
95
  else
92
- raise InvalidStepError.new(step.step_name)
96
+ raise MissingStepError.new(step.name)
93
97
  end
94
98
  end
95
99
 
96
100
  def assert_valid_step_args(step_args)
97
101
  step_args.each_key do |step_name|
98
- unless steps.any? { |step| step.step_name == step_name }
102
+ unless steps.any? { |step| step.name == step_name }
99
103
  raise ArgumentError, "+#{step_name}+ is not a valid step name"
100
104
  end
101
105
  end
@@ -107,7 +111,7 @@ module Dry
107
111
  num_args_supplied = step.call_args.length + 1 # add 1 for main `input`
108
112
 
109
113
  if num_args_required > num_args_supplied
110
- raise ArgumentError, "not enough arguments supplied for step +#{step.step_name}+"
114
+ raise ArgumentError, "not enough arguments supplied for step +#{step.name}+"
111
115
  end
112
116
  end
113
117
  end
@@ -1,6 +1,8 @@
1
- require "dry/monads/result"
2
- require "dry/matcher"
3
- require "dry/matcher/result_matcher"
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/monads/result'
4
+ require 'dry/matcher'
5
+ require 'dry/matcher/result_matcher'
4
6
 
5
7
  module Dry
6
8
  module Transaction
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Dry
2
4
  module Transaction
3
5
  class OperationResolver < Module
@@ -5,7 +7,7 @@ module Dry
5
7
  module_exec(container) do |ops_container|
6
8
  define_method :initialize do |**kwargs|
7
9
  operation_kwargs = self.class.steps.select(&:operation_name).map { |step|
8
- operation = kwargs.fetch(step.step_name) {
10
+ operation = kwargs.fetch(step.name) {
9
11
  if ops_container && ops_container.key?(step.operation_name)
10
12
  ops_container[step.operation_name]
11
13
  else
@@ -13,7 +15,7 @@ module Dry
13
15
  end
14
16
  }
15
17
 
16
- [step.step_name, operation]
18
+ [step.name, operation]
17
19
  }.to_h
18
20
 
19
21
  super(**kwargs, **operation_kwargs)
@@ -1,21 +1,23 @@
1
- require "dry/matcher"
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/matcher'
2
4
 
3
5
  module Dry
4
6
  module Transaction
5
7
  ResultMatcher = Dry::Matcher.new(
6
8
  success: Dry::Matcher::Case.new(
7
- match: -> result { result.right? },
9
+ match: -> result { result.success? },
8
10
  resolve: -> result { result.value! }
9
11
  ),
10
12
  failure: Dry::Matcher::Case.new(
11
13
  match: -> result, step_name = nil {
12
14
  if step_name
13
- result.left? && result.left.step.step_name == step_name
15
+ result.failure? && result.failure.step.name == step_name
14
16
  else
15
- result.left?
17
+ result.failure?
16
18
  end
17
19
  },
18
- resolve: -> result { result.left.value }
20
+ resolve: -> result { result.failure.value }
19
21
  )
20
22
  )
21
23
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Dry
2
4
  module Transaction
3
5
  # @api private
@@ -1,7 +1,9 @@
1
- require "dry/monads/result"
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/monads/result'
2
4
  require 'dry/events/publisher'
3
- require "dry/transaction/step_failure"
4
- require "dry/transaction/step_adapter"
5
+ require 'dry/transaction/step_failure'
6
+ require 'dry/transaction/step_adapter'
5
7
 
6
8
  module Dry
7
9
  module Transaction
@@ -17,14 +19,14 @@ module Dry
17
19
  register_event(:step_succeeded)
18
20
  register_event(:step_failed)
19
21
 
20
- attr_reader :step_adapter
21
- attr_reader :step_name
22
+ attr_reader :adapter
23
+ attr_reader :name
22
24
  attr_reader :operation_name
23
25
  attr_reader :call_args
24
26
 
25
- def initialize(step_adapter, step_name, operation_name, operation, options, call_args = [])
26
- @step_adapter = StepAdapter[step_adapter, operation, **options, step_name: step_name, operation_name: operation_name]
27
- @step_name = step_name
27
+ def initialize(adapter:, name:, operation_name:, operation: nil, options:, call_args: [])
28
+ @adapter = StepAdapter[adapter, operation, { **options, step_name: name, operation_name: operation_name }]
29
+ @name = name
28
30
  @operation_name = operation_name
29
31
  @call_args = call_args
30
32
  end
@@ -32,47 +34,58 @@ module Dry
32
34
  def with(operation: UNDEFINED, call_args: UNDEFINED)
33
35
  return self if operation == UNDEFINED && call_args == UNDEFINED
34
36
 
35
- new_operation = operation == UNDEFINED ? step_adapter.operation : operation
37
+ new_operation = operation == UNDEFINED ? adapter.operation : operation
36
38
  new_call_args = call_args == UNDEFINED ? self.call_args : Array(call_args)
37
39
 
38
40
  self.class.new(
39
- step_adapter,
40
- step_name,
41
- operation_name,
42
- new_operation,
43
- step_adapter.options,
44
- new_call_args
41
+ adapter: adapter,
42
+ name: name,
43
+ operation_name: operation_name,
44
+ operation: new_operation,
45
+ options: adapter.options,
46
+ call_args: new_call_args,
45
47
  )
46
48
  end
47
49
 
48
50
  def call(input, continue = RETURN)
49
51
  args = [input, *call_args]
50
52
 
51
- if step_adapter.yields?
52
- with_broadcast(args) { step_adapter.(args, &continue) }
53
+ if adapter.yields?
54
+ with_broadcast(args) { adapter.(args, &continue) }
53
55
  else
54
- continue.(with_broadcast(args) { step_adapter.(args) })
56
+ continue.(with_broadcast(args) { adapter.(args) })
55
57
  end
56
58
  end
57
59
 
58
60
  def with_broadcast(args)
59
- publish(:step, step_name: step_name, args: args)
61
+ publish(:step, step_name: name, args: args)
60
62
 
61
63
  yield.fmap { |value|
62
- publish(:step_succeeded, step_name: step_name, args: args, value: value)
64
+ publish(:step_succeeded, step_name: name, args: args, value: value)
63
65
  value
64
66
  }.or { |value|
65
- publish(:step_failed, step_name: step_name, args: args, value: value)
66
- Failure(StepFailure.new(self, value))
67
+ Failure(
68
+ StepFailure.(self, value) {
69
+ publish(:step_failed, step_name: name, args: args, value: value)
70
+ }
71
+ )
67
72
  }
68
73
  end
69
74
 
75
+ def internal?
76
+ !external?
77
+ end
78
+
79
+ def external?
80
+ !!operation_name
81
+ end
82
+
70
83
  def arity
71
- step_adapter.operation.arity
84
+ adapter.operation.arity
72
85
  end
73
86
 
74
87
  def operation
75
- step_adapter.operation
88
+ adapter.operation
76
89
  end
77
90
  end
78
91
  end
@@ -1,4 +1,6 @@
1
- require "dry/transaction/callable"
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/transaction/callable'
2
4
 
3
5
  module Dry
4
6
  module Transaction
@@ -28,9 +30,9 @@ module Dry
28
30
 
29
31
  @options = options
30
32
 
31
- @yields = @adapter.
32
- parameters.
33
- any? { |type, _| type == :block }
33
+ @yields = @adapter
34
+ .parameters
35
+ .any? { |type, _| type == :block }
34
36
  end
35
37
 
36
38
  def yields?
@@ -1,4 +1,6 @@
1
- require "dry-container"
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry-container'
2
4
 
3
5
  module Dry
4
6
  module Transaction
@@ -8,9 +10,9 @@ module Dry
8
10
  end
9
11
  end
10
12
 
11
- require "dry/transaction/step_adapters/check"
12
- require "dry/transaction/step_adapters/map"
13
- require "dry/transaction/step_adapters/raw"
14
- require "dry/transaction/step_adapters/tee"
15
- require "dry/transaction/step_adapters/try"
16
- require "dry/transaction/step_adapters/around"
13
+ require 'dry/transaction/step_adapters/check'
14
+ require 'dry/transaction/step_adapters/map'
15
+ require 'dry/transaction/step_adapters/raw'
16
+ require 'dry/transaction/step_adapters/tee'
17
+ require 'dry/transaction/step_adapters/try'
18
+ require 'dry/transaction/step_adapters/around'
@@ -1,5 +1,7 @@
1
- require "dry/monads/result"
2
- require "dry/transaction/errors"
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/monads/result'
4
+ require 'dry/transaction/errors'
3
5
 
4
6
  module Dry
5
7
  module Transaction
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Dry
2
4
  module Transaction
3
5
  class StepAdapters
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Dry
2
4
  module Transaction
3
5
  class StepAdapters