u-case 5.5.0 → 5.7.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.
@@ -60,6 +60,67 @@ module Micro
60
60
  Kind::Hash[arg]
61
61
  end
62
62
 
63
+ def flow_steps_kwarg!(args, steps, label)
64
+ return unless args && steps
65
+
66
+ raise ArgumentError,
67
+ "#{label} accepts a positional collection OR `steps:`, not both"
68
+ end
69
+
70
+ def transaction_kwarg!(value)
71
+ return nil if value.nil? || value == false
72
+ return true if value == true
73
+
74
+ if value.is_a?(Class)
75
+ transaction_owner!(value)
76
+ return value
77
+ end
78
+
79
+ if value.is_a?(Hash)
80
+ extra = value.keys - [:with]
81
+
82
+ raise ArgumentError,
83
+ "transaction: unsupported key(s) #{extra.inspect} (only `:with` is accepted)" unless extra.empty?
84
+
85
+ with = value[:with]
86
+ transaction_owner!(with)
87
+
88
+ return with
89
+ end
90
+
91
+ raise ArgumentError,
92
+ "transaction: #{value.inspect} is not supported (accepts `true`, `false`, `nil`, or `{ with: SomeARClass }`)"
93
+ end
94
+
95
+ def activerecord_loaded!
96
+ return if defined?(::ActiveRecord::Base)
97
+
98
+ raise ::Micro::Cases::Error::TransactionAdapterMissing
99
+ end
100
+
101
+ # Validates a transaction owner class. We accept Class instances
102
+ # only; the AR-subclass check is enforced if (and only if)
103
+ # ActiveRecord is already loaded — otherwise we defer to runtime
104
+ # so that load-order quirks (Rails initializers running before
105
+ # the AR autoload) don't break class-eval-time declarations.
106
+ def transaction_owner!(klass)
107
+ raise ArgumentError,
108
+ "transaction owner #{klass.inspect} must be a subclass of ActiveRecord::Base" unless klass.is_a?(Class)
109
+
110
+ return unless defined?(::ActiveRecord::Base)
111
+ return if klass <= ::ActiveRecord::Base
112
+
113
+ raise ArgumentError,
114
+ "transaction owner #{klass.inspect} must be a subclass of ActiveRecord::Base"
115
+ end
116
+
117
+ def transaction_class_callback!(callable)
118
+ return if callable.respond_to?(:call)
119
+
120
+ raise ArgumentError,
121
+ "Micro::Case.config.default_transaction_class= expects a callable (a block, lambda or proc), got #{callable.inspect}"
122
+ end
123
+
63
124
  def results_contract!(use_case_class, kind, type, value)
64
125
  contract = use_case_class.__results_contract__
65
126
  return unless contract
@@ -108,6 +169,16 @@ module Micro
108
169
  def flow_use_cases!(_use_cases); end
109
170
  def map_args!(_args); end
110
171
  def hash!(arg); arg; end
172
+ def flow_steps_kwarg!(_args, _steps, _label); end
173
+ def transaction_kwarg!(value)
174
+ return true if value == true
175
+ return value if value.is_a?(Class)
176
+ return value[:with] if value.is_a?(Hash) && value[:with].is_a?(Class)
177
+ nil
178
+ end
179
+ def activerecord_loaded!; end
180
+ def transaction_owner!(_klass); end
181
+ def transaction_class_callback!(_callable); end
111
182
  def results_contract!(_use_case_class, _kind, _type, _value); end
112
183
  end
113
184
  end
@@ -52,6 +52,22 @@ module Micro
52
52
 
53
53
  @activemodel_validation_errors_failure = :invalid_attributes
54
54
  end
55
+
56
+ DEFAULT_TRANSACTION_CLASS_CALLBACK = -> { ::ActiveRecord::Base }.freeze
57
+
58
+ def default_transaction_class=(callable)
59
+ ::Micro::Case.check.transaction_class_callback!(callable)
60
+
61
+ @default_transaction_class = callable
62
+ end
63
+
64
+ def default_transaction_class(&block)
65
+ return self.default_transaction_class = block if block
66
+
67
+ return @default_transaction_class if defined?(@default_transaction_class)
68
+
69
+ DEFAULT_TRANSACTION_CLASS_CALLBACK
70
+ end
55
71
  end
56
72
  end
57
73
  end
@@ -50,6 +50,32 @@ module Micro
50
50
  @__success ? :success : :failure
51
51
  end
52
52
 
53
+ def deconstruct
54
+ [@__success ? :success : :failure, type, data]
55
+ end
56
+
57
+ def deconstruct_keys(keys)
58
+ if keys.nil?
59
+ hash = { type: type, data: data, result: data, use_case: use_case, transitions: transitions }
60
+ hash[@__success ? :success : :failure] = type
61
+ return hash
62
+ end
63
+
64
+ hash = {}
65
+ keys.each do |key|
66
+ case key
67
+ when :type then hash[:type] = type
68
+ when :data then hash[:data] = data
69
+ when :result then hash[:result] = data
70
+ when :use_case then hash[:use_case] = use_case
71
+ when :transitions then hash[:transitions] = transitions
72
+ when :success then hash[:success] = type if @__success
73
+ when :failure then hash[:failure] = type if !@__success
74
+ end
75
+ end
76
+ hash
77
+ end
78
+
53
79
  def [](key)
54
80
  data[key]
55
81
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Micro
4
4
  class Case
5
- VERSION = '5.5.0'.freeze
5
+ VERSION = '5.7.0'.freeze
6
6
  end
7
7
  end
data/lib/micro/case.rb CHANGED
@@ -63,8 +63,11 @@ module Micro
63
63
  Proc.new { |arg| call(arg) }
64
64
  end
65
65
 
66
- def self.flow(*args)
67
- @__flow_use_cases = Cases::Utils.map_use_cases(args)
66
+ def self.flow(*args, transaction: nil, steps: nil)
67
+ ::Micro::Case.check.flow_steps_kwarg!(args.empty? ? nil : args, steps, "#{self.name}.flow")
68
+
69
+ @__flow_use_cases = Cases::Utils.map_use_cases(steps || args)
70
+ @__flow_transaction = transaction
68
71
  end
69
72
 
70
73
  def self.results(&block)
@@ -81,6 +84,19 @@ module Micro
81
84
  parent.respond_to?(:__results_contract__) ? parent.__results_contract__ : nil
82
85
  end
83
86
 
87
+ def self.transaction(with:)
88
+ ::Micro::Case.check.transaction_owner!(with)
89
+
90
+ @__transaction_class = with
91
+ end
92
+
93
+ def self.__transaction_class__
94
+ return @__transaction_class if defined?(@__transaction_class)
95
+
96
+ parent = superclass
97
+ parent.respond_to?(:__transaction_class__) ? parent.__transaction_class__ : nil
98
+ end
99
+
84
100
  class << self
85
101
  alias __call__ call
86
102
 
@@ -129,7 +145,17 @@ module Micro
129
145
 
130
146
  self.class_eval('def use_cases; self.class.use_cases; end')
131
147
 
132
- @__flow = __flow_builder__.build(args)
148
+ @__flow = __flow_builder__.build(args, transaction: __resolved_flow_transaction)
149
+ end
150
+
151
+ private_class_method def self.__flow_transaction
152
+ return @__flow_transaction if defined?(@__flow_transaction)
153
+ end
154
+
155
+ private_class_method def self.__resolved_flow_transaction
156
+ return __transaction_class__ || true if __flow_transaction == true
157
+
158
+ __flow_transaction
133
159
  end
134
160
 
135
161
  FLOW_STEP = 'Self'.freeze
@@ -301,15 +327,31 @@ module Micro
301
327
  @__result.__set__(is_success, value, type, self)
302
328
  end
303
329
 
304
- def transaction(adapter = :activerecord)
305
- raise NotImplementedError unless adapter == :activerecord
330
+ def transaction(adapter = nil, with: nil)
331
+ # Backward-compat shim for the pre-5.6.0 positional form:
332
+ # transaction(:activerecord) { ... }
333
+ # The `:activerecord` value was the only positional value the
334
+ # helper ever accepted on prior versions. Anything else raises.
335
+ if adapter
336
+ raise ArgumentError,
337
+ "transaction(#{adapter.inspect}) is not supported; use transaction(with: SomeARClass) or transaction without arguments" unless adapter == :activerecord
338
+ end
339
+
340
+ ::Micro::Case.check.transaction_owner!(with) if with
341
+
342
+ owner = with || self.class.__transaction_class__
343
+
344
+ if owner.nil?
345
+ ::Micro::Case.check.activerecord_loaded!
346
+ owner = Config.instance.default_transaction_class.call
347
+ end
306
348
 
307
349
  result = nil
308
350
 
309
- ActiveRecord::Base.transaction do
351
+ owner.transaction do
310
352
  result = yield
311
353
 
312
- raise ActiveRecord::Rollback if result.failure?
354
+ raise ::ActiveRecord::Rollback if result.failure?
313
355
  end
314
356
 
315
357
  result
@@ -7,6 +7,15 @@ module Micro
7
7
  class InvalidUseCases < ArgumentError
8
8
  def initialize; super('argument must be a collection of `Micro::Case` classes'.freeze); end
9
9
  end
10
+
11
+ class TransactionAdapterMissing < RuntimeError
12
+ def initialize
13
+ super(
14
+ 'transaction: true requires ActiveRecord to be loaded. '\
15
+ "Add `require 'active_record'` (or `gem 'activerecord'` to your Gemfile) before invoking the flow.".freeze
16
+ )
17
+ end
18
+ end
10
19
  end
11
20
 
12
21
  end
@@ -8,30 +8,31 @@ module Micro
8
8
 
9
9
  attr_reader :use_cases
10
10
 
11
- def self.build(args)
11
+ def self.build(args, transaction: nil)
12
12
  use_cases = Utils.map_use_cases(args)
13
13
 
14
14
  ::Micro::Case.check.flow_use_cases!(use_cases)
15
15
 
16
- new(use_cases)
16
+ new(use_cases, transaction: transaction)
17
17
  end
18
18
 
19
- def initialize(use_cases)
19
+ def initialize(use_cases, transaction: nil)
20
20
  @use_cases = use_cases.dup.freeze
21
21
  @next_ones = use_cases.dup
22
22
  @first = @next_ones.shift
23
+ @transaction = ::Micro::Case.check.transaction_kwarg!(transaction)
23
24
  end
24
25
 
25
26
  def inspect
26
- '#<(%s) use_cases=%s>' % [self.class, @use_cases]
27
+ return '#<(%s) use_cases=%s>' % [self.class, @use_cases] unless @transaction
28
+
29
+ '#<(%s) transaction=%p use_cases=%s>' % [self.class, @transaction, @use_cases]
27
30
  end
28
31
 
29
32
  def call!(input:, result:)
30
- first_result = __call_use_case(@first, result, input)
31
-
32
- return first_result if @next_ones.empty?
33
+ return __call_steps(input, result) unless @transaction
33
34
 
34
- __call_next_use_cases(first_result)
35
+ __wrap_in_transaction { __call_steps(input, result) }
35
36
  end
36
37
 
37
38
  def call(input = Kind::Empty::HASH)
@@ -75,6 +76,43 @@ module Micro
75
76
  raise Case::Error::InvalidInvocationOfTheThenMethod.new("#{self.class.name}#")
76
77
  end
77
78
 
79
+ def __call_steps(input, result)
80
+ first_result = __call_use_case(@first, result, input)
81
+
82
+ return first_result if @next_ones.empty?
83
+
84
+ __call_next_use_cases(first_result)
85
+ end
86
+
87
+ def __wrap_in_transaction
88
+ owner = __transaction_owner
89
+
90
+ result = nil
91
+
92
+ owner.transaction do
93
+ result = yield
94
+
95
+ raise ::ActiveRecord::Rollback if result.failure?
96
+ end
97
+
98
+ result
99
+ end
100
+
101
+ def __transaction_owner
102
+ return @transaction if @transaction.is_a?(Class)
103
+
104
+ callback = ::Micro::Case::Config.instance.default_transaction_class
105
+
106
+ # Only the gem's default callback (`-> { ActiveRecord::Base }`)
107
+ # needs the AR-loaded guard. A user-supplied callback can
108
+ # return whatever class they want — we trust it.
109
+ if callback.equal?(::Micro::Case::Config::DEFAULT_TRANSACTION_CLASS_CALLBACK)
110
+ ::Micro::Case.check.activerecord_loaded!
111
+ end
112
+
113
+ callback.call
114
+ end
115
+
78
116
  def __call_use_case(use_case, result, input)
79
117
  __build_use_case(use_case, result, input).__call__
80
118
  end
data/lib/micro/cases.rb CHANGED
@@ -8,16 +8,24 @@ require 'micro/cases/map'
8
8
 
9
9
  module Micro
10
10
  module Cases
11
- def self.flow(args)
12
- Flow.build(args)
11
+ def self.flow(args = nil, transaction: nil, steps: nil)
12
+ args = nil if args.is_a?(Array) && args.empty?
13
+
14
+ ::Micro::Case.check.flow_steps_kwarg!(args, steps, 'Micro::Cases.flow')
15
+
16
+ Flow.build(steps || args, transaction: transaction)
13
17
  end
14
18
 
15
- def self.safe_flow(args)
19
+ def self.safe_flow(args = nil, transaction: nil, steps: nil)
16
20
  if Case::Config.instance.disable_safe_features
17
21
  raise Case::Error::SafeFeaturesDisabled.new('Micro::Cases.safe_flow')
18
22
  end
19
23
 
20
- Safe::Flow.build(args)
24
+ args = nil if args.is_a?(Array) && args.empty?
25
+
26
+ ::Micro::Case.check.flow_steps_kwarg!(args, steps, 'Micro::Cases.safe_flow')
27
+
28
+ Safe::Flow.build(steps || args, transaction: transaction)
21
29
  end
22
30
 
23
31
  def self.map(args)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: u-case
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.5.0
4
+ version: 5.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rodrigo Serradura