u-case 5.4.0 → 5.6.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.
@@ -59,6 +59,99 @@ module Micro
59
59
  def hash!(arg)
60
60
  Kind::Hash[arg]
61
61
  end
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
+
124
+ def results_contract!(use_case_class, kind, type, value)
125
+ contract = use_case_class.__results_contract__
126
+ return unless contract
127
+ return unless type.is_a?(Symbol)
128
+ return if value.is_a?(Exception)
129
+
130
+ if kind == :success
131
+ declared = contract.success_declared?(type)
132
+ declared_types = contract.successes.keys
133
+ required = contract.success_keys(type) if declared
134
+ else
135
+ declared = contract.failure_declared?(type)
136
+ declared_types = contract.failures.keys
137
+ required = contract.failure_keys(type) if declared
138
+ end
139
+
140
+ raise Error::UnexpectedResultType.new(use_case_class, kind, type, declared_types) unless declared
141
+ return if required.nil? || required.empty?
142
+
143
+ if value.is_a?(Hash)
144
+ data_keys = value.keys.map { |k| k.is_a?(String) ? k.to_sym : k }
145
+ elsif value.is_a?(Symbol)
146
+ data_keys = [type]
147
+ else
148
+ return
149
+ end
150
+
151
+ missing = required - data_keys
152
+
153
+ raise Error::MissingResultKeys.new(use_case_class, kind, type, missing) unless missing.empty?
154
+ end
62
155
  end
63
156
 
64
157
  module Disabled
@@ -76,6 +169,17 @@ module Micro
76
169
  def flow_use_cases!(_use_cases); end
77
170
  def map_args!(_args); end
78
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
182
+ def results_contract!(_use_case_class, _kind, _type, _value); end
79
183
  end
80
184
  end
81
185
  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
@@ -60,9 +60,32 @@ module Micro
60
60
  end
61
61
  end
62
62
 
63
+ class UnexpectedResultType < TypeError
64
+ def initialize(use_case_class, kind, type, declared_types)
65
+ declared_list = declared_types.map { |t| ":#{t}" }.join(', ')
66
+ declared_list = '(none)' if declared_list.empty?
67
+
68
+ super(
69
+ "#{use_case_class.name} declared a results contract — " \
70
+ "#{kind} type :#{type} is not declared. Declared #{kind} types: #{declared_list}."
71
+ )
72
+ end
73
+ end
74
+
75
+ class MissingResultKeys < ArgumentError
76
+ def initialize(use_case_class, kind, type, missing_keys)
77
+ missing_list = missing_keys.map { |k| ":#{k}" }.join(', ')
78
+
79
+ super(
80
+ "#{use_case_class.name} declared a results contract — " \
81
+ "#{kind} :#{type} is missing required result keys: #{missing_list}."
82
+ )
83
+ end
84
+ end
85
+
63
86
  def self.by_wrong_usage?(exception)
64
87
  case exception
65
- when Kind::Error, ArgumentError, InvalidResult, UnexpectedResult then true
88
+ when Kind::Error, ArgumentError, InvalidResult, UnexpectedResult, UnexpectedResultType then true
66
89
  else false
67
90
  end
68
91
  end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Micro
4
+ class Case
5
+ class Result
6
+ class Contract
7
+ attr_reader :successes, :failures
8
+
9
+ def self.define(&block)
10
+ contract = new
11
+ block.call(Definition.new(contract))
12
+ contract
13
+ end
14
+
15
+ def initialize
16
+ @successes = {}
17
+ @failures = {}
18
+ end
19
+
20
+ def add_success(type, keys)
21
+ @successes[type] = Array(keys).map(&:to_sym)
22
+ end
23
+
24
+ def add_failure(type, keys)
25
+ @failures[type] = Array(keys).map(&:to_sym)
26
+ end
27
+
28
+ def success_declared?(type)
29
+ @successes.key?(type)
30
+ end
31
+
32
+ def failure_declared?(type)
33
+ @failures.key?(type)
34
+ end
35
+
36
+ def success_keys(type)
37
+ @successes[type]
38
+ end
39
+
40
+ def failure_keys(type)
41
+ @failures[type]
42
+ end
43
+
44
+ class Definition
45
+ def initialize(contract)
46
+ @contract = contract
47
+ end
48
+
49
+ def success(type = :ok, result: nil)
50
+ @contract.add_success(Kind::Symbol[type], result)
51
+ end
52
+
53
+ def failure(type = :error, result: nil)
54
+ @contract.add_failure(Kind::Symbol[type], result)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Micro
4
4
  class Case
5
- VERSION = '5.4.0'.freeze
5
+ VERSION = '5.6.0'.freeze
6
6
  end
7
7
  end
data/lib/micro/case.rb CHANGED
@@ -11,6 +11,7 @@ module Micro
11
11
  require 'micro/case/utils'
12
12
  require 'micro/case/error'
13
13
  require 'micro/case/result'
14
+ require 'micro/case/result/contract'
14
15
  require 'micro/case/check'
15
16
  require 'micro/case/config'
16
17
  require 'micro/case/safe'
@@ -62,8 +63,38 @@ module Micro
62
63
  Proc.new { |arg| call(arg) }
63
64
  end
64
65
 
65
- def self.flow(*args)
66
- @__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
71
+ end
72
+
73
+ def self.results(&block)
74
+ raise ArgumentError, 'a block is required'.freeze unless block
75
+ raise ArgumentError, 'must be called on a Micro::Case subclass, not on Micro::Case itself'.freeze if self == ::Micro::Case
76
+
77
+ @__results_contract = Result::Contract.define(&block)
78
+ end
79
+
80
+ def self.__results_contract__
81
+ return @__results_contract if defined?(@__results_contract)
82
+
83
+ parent = superclass
84
+ parent.respond_to?(:__results_contract__) ? parent.__results_contract__ : nil
85
+ end
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
67
98
  end
68
99
 
69
100
  class << self
@@ -114,7 +145,17 @@ module Micro
114
145
 
115
146
  self.class_eval('def use_cases; self.class.use_cases; end')
116
147
 
117
- @__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
118
159
  end
119
160
 
120
161
  FLOW_STEP = 'Self'.freeze
@@ -227,9 +268,10 @@ module Micro
227
268
  end
228
269
 
229
270
  def __failure_from_attributes_errors
230
- Failure(
231
- Config.instance.activemodel_validation_errors_failure,
232
- result: { errors: attributes_errors }
271
+ __get_result(
272
+ false,
273
+ { errors: attributes_errors },
274
+ Config.instance.activemodel_validation_errors_failure
233
275
  )
234
276
  end
235
277
 
@@ -244,6 +286,8 @@ module Micro
244
286
  def Success(type = :ok, result: nil)
245
287
  value = result || type
246
288
 
289
+ ::Micro::Case.check.results_contract!(self.class, :success, type, value)
290
+
247
291
  __get_result(true, value, type)
248
292
  end
249
293
 
@@ -260,10 +304,11 @@ module Micro
260
304
 
261
305
  type = MapFailureType.call(value, type)
262
306
 
307
+ ::Micro::Case.check.results_contract!(self.class, :failure, type, value)
308
+
263
309
  __get_result(false, value, type)
264
310
  end
265
311
 
266
-
267
312
  def Check(type = nil, result: nil, on: Kind::Empty::HASH)
268
313
  result_key = type || :check
269
314
 
@@ -282,15 +327,31 @@ module Micro
282
327
  @__result.__set__(is_success, value, type, self)
283
328
  end
284
329
 
285
- def transaction(adapter = :activerecord)
286
- 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
287
348
 
288
349
  result = nil
289
350
 
290
- ActiveRecord::Base.transaction do
351
+ owner.transaction do
291
352
  result = yield
292
353
 
293
- raise ActiveRecord::Rollback if result.failure?
354
+ raise ::ActiveRecord::Rollback if result.failure?
294
355
  end
295
356
 
296
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.4.0
4
+ version: 5.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rodrigo Serradura
@@ -122,6 +122,7 @@ files:
122
122
  - lib/micro/case/config.rb
123
123
  - lib/micro/case/error.rb
124
124
  - lib/micro/case/result.rb
125
+ - lib/micro/case/result/contract.rb
125
126
  - lib/micro/case/result/transitions.rb
126
127
  - lib/micro/case/result/wrapper.rb
127
128
  - lib/micro/case/safe.rb