dexkit 0.7.0 → 0.9.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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +45 -0
  3. data/README.md +40 -7
  4. data/gemfiles/mongoid_no_ar.gemfile +10 -0
  5. data/gemfiles/mongoid_no_ar.gemfile.lock +232 -0
  6. data/guides/llm/EVENT.md +60 -5
  7. data/guides/llm/FORM.md +3 -3
  8. data/guides/llm/OPERATION.md +127 -18
  9. data/guides/llm/QUERY.md +3 -3
  10. data/lib/dex/event/bus.rb +7 -0
  11. data/lib/dex/event/export.rb +56 -0
  12. data/lib/dex/event/handler.rb +33 -0
  13. data/lib/dex/event/test_helpers.rb +88 -0
  14. data/lib/dex/event.rb +27 -0
  15. data/lib/dex/event_test_helpers.rb +1 -86
  16. data/lib/dex/form/uniqueness_validator.rb +17 -1
  17. data/lib/dex/operation/async_proxy.rb +1 -0
  18. data/lib/dex/operation/explain.rb +208 -0
  19. data/lib/dex/operation/export.rb +144 -0
  20. data/lib/dex/operation/guard_wrapper.rb +15 -4
  21. data/lib/dex/operation/lock_wrapper.rb +15 -2
  22. data/lib/dex/operation/once_wrapper.rb +23 -15
  23. data/lib/dex/operation/record_backend.rb +25 -0
  24. data/lib/dex/operation/record_wrapper.rb +29 -4
  25. data/lib/dex/operation/test_helpers/assertions.rb +335 -0
  26. data/lib/dex/operation/test_helpers/execution.rb +30 -0
  27. data/lib/dex/operation/test_helpers/stubbing.rb +61 -0
  28. data/lib/dex/operation/test_helpers.rb +150 -0
  29. data/lib/dex/operation/transaction_adapter.rb +29 -68
  30. data/lib/dex/operation/transaction_wrapper.rb +10 -16
  31. data/lib/dex/operation.rb +46 -2
  32. data/lib/dex/props_setup.rb +25 -2
  33. data/lib/dex/query/backend.rb +13 -0
  34. data/lib/dex/query.rb +9 -5
  35. data/lib/dex/railtie.rb +84 -0
  36. data/lib/dex/ref_type.rb +4 -0
  37. data/lib/dex/registry.rb +63 -0
  38. data/lib/dex/test_helpers.rb +4 -139
  39. data/lib/dex/tool.rb +115 -0
  40. data/lib/dex/type_coercion.rb +4 -1
  41. data/lib/dex/type_serializer.rb +132 -0
  42. data/lib/dex/version.rb +1 -1
  43. data/lib/dexkit.rb +11 -5
  44. metadata +16 -5
  45. data/lib/dex/test_helpers/assertions.rb +0 -333
  46. data/lib/dex/test_helpers/execution.rb +0 -28
  47. data/lib/dex/test_helpers/stubbing.rb +0 -59
  48. /data/lib/dex/{event_test_helpers → event/test_helpers}/assertions.rb +0 -0
@@ -0,0 +1,335 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dex
4
+ class Operation
5
+ module TestHelpers
6
+ # --- Result assertions ---
7
+
8
+ def assert_ok(result, expected = :_not_given, msg = nil, &block)
9
+ assert result.ok?, msg || "Expected Ok, got Err:\n#{_dex_format_err(result)}"
10
+ if expected != :_not_given
11
+ assert_equal expected, result.value, msg || "Ok value mismatch"
12
+ end
13
+ yield result.value if block
14
+ result
15
+ end
16
+
17
+ def refute_ok(result, msg = nil)
18
+ refute result.ok?, msg || "Expected Err, got Ok:\n#{_dex_format_ok(result)}"
19
+ result
20
+ end
21
+
22
+ def assert_err(result, code = nil, message: nil, details: nil, msg: nil, &block)
23
+ assert result.error?, msg || "Expected Err, got Ok:\n#{_dex_format_ok(result)}"
24
+ if code
25
+ assert_equal code, result.code, msg || "Error code mismatch.\n#{_dex_format_err(result)}"
26
+ end
27
+ if message
28
+ case message
29
+ when Regexp
30
+ assert_match message, result.message, msg || "Error message mismatch.\n#{_dex_format_err(result)}"
31
+ else
32
+ assert_equal message, result.message, msg || "Error message mismatch.\n#{_dex_format_err(result)}"
33
+ end
34
+ end
35
+ details&.each do |key, val|
36
+ assert_equal val, result.details&.dig(key),
37
+ msg || "Error details[:#{key}] mismatch.\n#{_dex_format_err(result)}"
38
+ end
39
+ yield result.error if block
40
+ result
41
+ end
42
+
43
+ def refute_err(result, code = nil, msg: nil)
44
+ if code
45
+ if result.error?
46
+ refute_equal code, result.code,
47
+ msg || "Expected result to not have error code #{code.inspect}, but it does.\n#{_dex_format_err(result)}"
48
+ end
49
+ else
50
+ refute result.error?, msg || "Expected Ok, got Err:\n#{_dex_format_err(result)}"
51
+ end
52
+ result
53
+ end
54
+
55
+ # --- One-liner assertions ---
56
+
57
+ def assert_operation(*args, returns: :_not_given, **params)
58
+ klass = _dex_resolve_subject(args)
59
+ result = klass.new(**params).safe.call
60
+ assert result.ok?, "Expected operation to succeed, got Err:\n#{_dex_format_err(result)}"
61
+ if returns != :_not_given
62
+ assert_equal returns, result.value, "Return value mismatch"
63
+ end
64
+ result
65
+ end
66
+
67
+ def assert_operation_error(*args, message: nil, details: nil, **params)
68
+ klass, code = _dex_resolve_subject_and_code(args)
69
+ result = klass.new(**params).safe.call
70
+ assert result.error?, "Expected operation to fail, got Ok:\n#{_dex_format_ok(result)}"
71
+ if code
72
+ assert_equal code, result.code, "Error code mismatch.\n#{_dex_format_err(result)}"
73
+ end
74
+ if message
75
+ case message
76
+ when Regexp
77
+ assert_match message, result.message
78
+ else
79
+ assert_equal message, result.message
80
+ end
81
+ end
82
+ details&.each do |key, val|
83
+ assert_equal val, result.details&.dig(key)
84
+ end
85
+ result
86
+ end
87
+
88
+ # --- Contract assertions ---
89
+
90
+ def assert_params(*args)
91
+ if args.last.is_a?(Hash)
92
+ klass_args, type_hash = _dex_split_class_and_hash(args)
93
+ klass = _dex_resolve_subject(klass_args)
94
+ contract = klass.contract
95
+ type_hash.each do |name, type|
96
+ assert contract.params.key?(name),
97
+ "Expected param #{name.inspect} to be declared on #{klass.name || klass}"
98
+ assert_equal type, contract.params[name],
99
+ "Type mismatch for param #{name.inspect}"
100
+ end
101
+ else
102
+ klass_args, names = _dex_split_class_and_symbols(args)
103
+ klass = _dex_resolve_subject(klass_args)
104
+ contract = klass.contract
105
+ assert_equal names.sort, contract.params.keys.sort,
106
+ "Params mismatch on #{klass.name || klass}.\n Expected: #{names.sort.inspect}\n Actual: #{contract.params.keys.sort.inspect}"
107
+ end
108
+ end
109
+
110
+ def assert_accepts_param(*args)
111
+ klass_args, names = _dex_split_class_and_symbols(args)
112
+ klass = _dex_resolve_subject(klass_args)
113
+ contract = klass.contract
114
+ names.each do |name|
115
+ assert contract.params.key?(name),
116
+ "Expected #{klass.name || klass} to accept param #{name.inspect}, but it doesn't.\n Declared params: #{contract.params.keys.inspect}"
117
+ end
118
+ end
119
+
120
+ def assert_success_type(*args)
121
+ klass = if args.first.is_a?(Class) && args.first < Dex::Operation
122
+ args.shift
123
+ else
124
+ _dex_resolve_subject([])
125
+ end
126
+ expected = args.first
127
+ contract = klass.contract
128
+ assert_equal expected, contract.success,
129
+ "Success type mismatch on #{klass.name || klass}"
130
+ end
131
+
132
+ def assert_error_codes(*args)
133
+ klass_args, codes = _dex_split_class_and_symbols(args)
134
+ klass = _dex_resolve_subject(klass_args)
135
+ contract = klass.contract
136
+ assert_equal codes.sort, contract.errors.sort,
137
+ "Error codes mismatch on #{klass.name || klass}.\n Expected: #{codes.sort.inspect}\n Actual: #{contract.errors.sort.inspect}"
138
+ end
139
+
140
+ def assert_contract(*args, params: nil, success: :_not_given, errors: nil)
141
+ klass = _dex_resolve_subject(args)
142
+ contract = klass.contract
143
+
144
+ if params
145
+ case params
146
+ when Array
147
+ assert_equal params.sort, contract.params.keys.sort, "Contract params mismatch"
148
+ when Hash
149
+ params.each do |name, type|
150
+ assert contract.params.key?(name), "Expected param #{name.inspect}"
151
+ assert_equal type, contract.params[name], "Type mismatch for param #{name.inspect}"
152
+ end
153
+ end
154
+ end
155
+
156
+ if success != :_not_given
157
+ assert_equal success, contract.success, "Contract success type mismatch"
158
+ end
159
+
160
+ if errors
161
+ assert_equal errors.sort, contract.errors.sort, "Contract error codes mismatch"
162
+ end
163
+ end
164
+
165
+ # --- Param validation assertions ---
166
+
167
+ def assert_invalid_params(*args, **params)
168
+ klass = _dex_resolve_subject(args)
169
+ assert_raises(Literal::TypeError) { klass.new(**params) }
170
+ end
171
+
172
+ def assert_valid_params(*args, **params)
173
+ klass = _dex_resolve_subject(args)
174
+ klass.new(**params)
175
+ end
176
+
177
+ # --- Async assertions ---
178
+
179
+ def assert_enqueues_operation(*args, queue: nil, **params)
180
+ _dex_ensure_active_job_test_helper!
181
+ klass = _dex_resolve_subject(args)
182
+ async_opts = queue ? { queue: queue } : {}
183
+ before_count = enqueued_jobs.size
184
+ klass.new(**params).async(**async_opts).call
185
+ new_jobs = enqueued_jobs[before_count..]
186
+ dex_job = new_jobs.find { |j|
187
+ j[:job] == Dex::Operation::DirectJob || j[:job] == Dex::Operation::RecordJob
188
+ }
189
+ assert dex_job,
190
+ "Expected #{klass.name || klass} to enqueue an async job, but none were enqueued"
191
+ end
192
+
193
+ def refute_enqueues_operation(&block)
194
+ _dex_ensure_active_job_test_helper!
195
+ before_count = enqueued_jobs.size
196
+ yield
197
+ after_count = enqueued_jobs.size
198
+ assert_equal before_count, after_count,
199
+ "Expected no operations to be enqueued, but #{after_count - before_count} were"
200
+ end
201
+
202
+ # --- Transaction assertions ---
203
+
204
+ def assert_rolls_back(model_class, &block)
205
+ count_before = model_class.count
206
+ assert_raises(Dex::Error) { yield }
207
+ assert_equal count_before, model_class.count,
208
+ "Expected transaction to roll back, but #{model_class.name} count changed from #{count_before} to #{model_class.count}"
209
+ end
210
+
211
+ def assert_commits(model_class, &block)
212
+ count_before = model_class.count
213
+ yield
214
+ assert count_before < model_class.count,
215
+ "Expected #{model_class.name} count to increase, but it stayed at #{count_before}"
216
+ end
217
+
218
+ # --- Guard assertions ---
219
+
220
+ def assert_callable(*args, **params)
221
+ klass = _dex_resolve_subject(args)
222
+ result = klass.callable(**params)
223
+ assert result.ok?, "Expected operation to be callable, but guards failed:\n#{_dex_format_err(result)}"
224
+ result
225
+ end
226
+
227
+ def refute_callable(*args, **params)
228
+ klass_args, codes = _dex_split_class_and_symbols(args)
229
+ klass = _dex_resolve_subject(klass_args)
230
+ code = codes.first
231
+ result = klass.callable(**params)
232
+ refute result.ok?, "Expected operation to NOT be callable, but all guards passed"
233
+ if code
234
+ failed_codes = result.details.map { |f| f[:guard] }
235
+ assert_includes failed_codes, code,
236
+ "Expected guard :#{code} to fail, but it didn't.\n Failed guards: #{failed_codes.inspect}"
237
+ end
238
+ result
239
+ end
240
+
241
+ # --- Batch assertions ---
242
+
243
+ def assert_all_succeed(*args, params_list:)
244
+ klass = _dex_resolve_subject(args)
245
+ results = params_list.map { |p| klass.new(**p).safe.call }
246
+ failures = results.each_with_index.reject { |r, _| r.ok? }
247
+ if failures.any?
248
+ msgs = failures.map { |r, i| " [#{i}] #{params_list[i].inspect} => #{_dex_format_err(r)}" }
249
+ flunk "Expected all #{results.size} calls to succeed, but #{failures.size} failed:\n#{msgs.join("\n")}"
250
+ end
251
+ results
252
+ end
253
+
254
+ def assert_all_fail(*args, code:, params_list:, message: nil, details: nil)
255
+ klass = _dex_resolve_subject(args)
256
+ results = params_list.map { |p| klass.new(**p).safe.call }
257
+ failures = results.each_with_index.reject { |r, _| r.error? && r.code == code }
258
+ if failures.any?
259
+ msgs = failures.map { |r, i|
260
+ status = r.ok? ? "Ok(#{r.value.inspect})" : "Err(#{r.code})"
261
+ " [#{i}] #{params_list[i].inspect} => #{status}"
262
+ }
263
+ flunk "Expected all #{results.size} calls to fail with #{code.inspect}, but #{failures.size} didn't:\n#{msgs.join("\n")}"
264
+ end
265
+ results.each_with_index do |r, i|
266
+ if message
267
+ case message
268
+ when Regexp
269
+ assert_match message, r.message,
270
+ "Error message mismatch at [#{i}] #{params_list[i].inspect}.\n#{_dex_format_err(r)}"
271
+ else
272
+ assert_equal message, r.message,
273
+ "Error message mismatch at [#{i}] #{params_list[i].inspect}.\n#{_dex_format_err(r)}"
274
+ end
275
+ end
276
+ details&.each do |key, val|
277
+ assert_equal val, r.details&.dig(key),
278
+ "Error details[:#{key}] mismatch at [#{i}] #{params_list[i].inspect}.\n#{_dex_format_err(r)}"
279
+ end
280
+ end
281
+ results
282
+ end
283
+
284
+ private
285
+
286
+ def _dex_format_err(result)
287
+ return "(not an error)" unless result.respond_to?(:error?) && result.error?
288
+
289
+ lines = [" code: #{result.code.inspect}"]
290
+ lines << " message: #{result.message.inspect}" if result.message && result.message != result.code.to_s
291
+ lines << " details: #{result.details.inspect}" if result.details
292
+ lines.join("\n")
293
+ end
294
+
295
+ def _dex_format_ok(result)
296
+ return "(not ok)" unless result.respond_to?(:ok?) && result.ok?
297
+
298
+ " value: #{result.value.inspect}"
299
+ end
300
+
301
+ def _dex_resolve_subject_and_code(args)
302
+ if args.first.is_a?(Class) && args.first < Dex::Operation
303
+ klass = args.shift
304
+ code = args.shift
305
+ [klass, code]
306
+ elsif args.first.is_a?(Symbol)
307
+ [_dex_resolve_subject([]), args.shift]
308
+ else
309
+ [_dex_resolve_subject([]), nil]
310
+ end
311
+ end
312
+
313
+ def _dex_split_class_and_symbols(args)
314
+ if args.first.is_a?(Class) && args.first < Dex::Operation
315
+ [args[0..0], args[1..]]
316
+ else
317
+ [[], args]
318
+ end
319
+ end
320
+
321
+ def _dex_split_class_and_hash(args)
322
+ hash = args.pop
323
+ klass_args = args.select { |a| a.is_a?(Class) && a < Dex::Operation }
324
+ [klass_args, hash]
325
+ end
326
+
327
+ def _dex_ensure_active_job_test_helper!
328
+ return if respond_to?(:assert_enqueued_with)
329
+
330
+ raise "assert_enqueues_operation requires ActiveJob::TestHelper. " \
331
+ "Include it in your test class: `include ActiveJob::TestHelper`"
332
+ end
333
+ end
334
+ end
335
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dex
4
+ class Operation
5
+ module TestHelpers
6
+ def call_operation(*args, **params)
7
+ klass = _dex_resolve_subject(args)
8
+ klass.new(**params).safe.call
9
+ end
10
+
11
+ def call_operation!(*args, **params)
12
+ klass = _dex_resolve_subject(args)
13
+ klass.new(**params).call
14
+ end
15
+
16
+ private
17
+
18
+ def _dex_resolve_subject(args)
19
+ if args.first.is_a?(Class) && args.first < Dex::Operation
20
+ args.first
21
+ elsif _dex_test_subject
22
+ _dex_test_subject
23
+ else
24
+ raise ArgumentError,
25
+ "No operation class specified. Pass it as the first argument or use `testing MyOperation` in your test class."
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dex
4
+ class Operation
5
+ module TestHelpers
6
+ def stub_operation(klass, returns: nil, error: nil, &block)
7
+ raise ArgumentError, "stub_operation requires a block" unless block
8
+
9
+ opts = if error
10
+ { error: error }
11
+ else
12
+ { returns: returns }
13
+ end
14
+
15
+ Dex::Operation::TestWrapper.register_stub(klass, **opts)
16
+ yield
17
+ ensure
18
+ Dex::Operation::TestWrapper.clear_stub(klass)
19
+ end
20
+
21
+ def spy_on_operation(klass, &block)
22
+ spy = Spy.new(klass)
23
+ yield spy
24
+ spy
25
+ end
26
+
27
+ class Spy
28
+ def initialize(klass)
29
+ @klass = klass
30
+ @started_at = Dex::TestLog.size
31
+ end
32
+
33
+ def calls
34
+ Dex::TestLog.calls[@started_at..].select { |e| e.operation_class == @klass }
35
+ end
36
+
37
+ def called?
38
+ calls.any?
39
+ end
40
+
41
+ def called_once?
42
+ calls.size == 1
43
+ end
44
+
45
+ def call_count
46
+ calls.size
47
+ end
48
+
49
+ def last_result
50
+ calls.last&.result
51
+ end
52
+
53
+ def called_with?(**params)
54
+ calls.any? do |entry|
55
+ params.all? { |k, v| entry.params[k] == v }
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../test_log"
4
+
5
+ module Dex
6
+ class Operation
7
+ module TestWrapper
8
+ @_installed = false
9
+
10
+ class << self
11
+ def install!
12
+ return if @_installed
13
+
14
+ Dex::Operation.prepend(self)
15
+ @_installed = true
16
+ end
17
+
18
+ def installed?
19
+ @_installed
20
+ end
21
+
22
+ # Stub registry
23
+
24
+ def stubs
25
+ @_stubs ||= {}
26
+ end
27
+
28
+ def find_stub(klass)
29
+ stubs[klass]
30
+ end
31
+
32
+ def register_stub(klass, **options)
33
+ stubs[klass] = options
34
+ end
35
+
36
+ def clear_stub(klass)
37
+ stubs.delete(klass)
38
+ end
39
+
40
+ def clear_all_stubs!
41
+ stubs.clear
42
+ end
43
+ end
44
+
45
+ def call
46
+ stub = Dex::Operation::TestWrapper.find_stub(self.class)
47
+ return _test_apply_stub(stub) if stub
48
+
49
+ started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
50
+ result = nil
51
+ err = nil
52
+
53
+ begin
54
+ result = super
55
+ rescue Exception => e # rubocop:disable Lint/RescueException
56
+ err = e
57
+ raise
58
+ ensure
59
+ duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at
60
+ _test_record_to_log(result, err, duration)
61
+ end
62
+
63
+ result
64
+ end
65
+
66
+ private
67
+
68
+ def _test_apply_stub(stub)
69
+ if stub[:error]
70
+ err_opts = stub[:error]
71
+ case err_opts
72
+ when Symbol
73
+ raise Dex::Error.new(err_opts)
74
+ when Hash
75
+ raise Dex::Error.new(err_opts[:code], err_opts[:message], details: err_opts[:details])
76
+ end
77
+ else
78
+ stub[:returns]
79
+ end
80
+ end
81
+
82
+ def _test_safe_params
83
+ respond_to?(:to_h) ? to_h : {}
84
+ rescue
85
+ {}
86
+ end
87
+
88
+ def _test_record_to_log(result, err, duration)
89
+ safe_result = if err
90
+ dex_err = if err.is_a?(Dex::Error)
91
+ err
92
+ else
93
+ Dex::Error.new(:exception, err.message, details: { exception_class: err.class.name })
94
+ end
95
+ Dex::Operation::Err.new(dex_err)
96
+ else
97
+ Dex::Operation::Ok.new(result)
98
+ end
99
+
100
+ entry = Dex::TestLog::Entry.new(
101
+ type: "Operation",
102
+ name: self.class.name || self.class.to_s,
103
+ operation_class: self.class,
104
+ params: _test_safe_params,
105
+ result: safe_result,
106
+ duration: duration,
107
+ caller_location: caller_locations(4, 1)&.first
108
+ )
109
+ Dex::TestLog.record(entry)
110
+ end
111
+ end
112
+
113
+ module TestHelpers
114
+ extend Dex::Concern
115
+
116
+ def self.included(base)
117
+ Dex::Operation::TestWrapper.install!
118
+ super
119
+ end
120
+
121
+ def setup
122
+ super
123
+ Dex::TestLog.clear!
124
+ Dex::Operation::TestWrapper.clear_all_stubs!
125
+ end
126
+
127
+ module ClassMethods
128
+ def testing(klass)
129
+ @_dex_test_subject = klass
130
+ end
131
+
132
+ def _dex_test_subject
133
+ return @_dex_test_subject if defined?(@_dex_test_subject) && @_dex_test_subject
134
+
135
+ superclass._dex_test_subject if superclass.respond_to?(:_dex_test_subject)
136
+ end
137
+ end
138
+
139
+ private
140
+
141
+ def _dex_test_subject
142
+ self.class._dex_test_subject
143
+ end
144
+ end
145
+ end
146
+ end
147
+
148
+ require_relative "test_helpers/execution"
149
+ require_relative "test_helpers/assertions"
150
+ require_relative "test_helpers/stubbing"
@@ -3,25 +3,44 @@
3
3
  module Dex
4
4
  class Operation
5
5
  module TransactionAdapter
6
+ KNOWN_ADAPTERS = %i[active_record].freeze
7
+
6
8
  def self.for(adapter_name)
7
- case adapter_name&.to_sym
9
+ case normalize_name(adapter_name)
8
10
  when :active_record
9
11
  ActiveRecordAdapter
10
- when :mongoid
11
- MongoidAdapter
12
12
  when nil
13
13
  detect
14
- else
15
- raise ArgumentError, "Unknown transaction adapter: #{adapter_name}"
16
14
  end
17
15
  end
18
16
 
17
+ def self.known_adapters
18
+ KNOWN_ADAPTERS
19
+ end
20
+
21
+ def self.normalize_name(adapter_name)
22
+ return nil if adapter_name.nil?
23
+
24
+ normalized = adapter_name.to_sym
25
+ return normalized if KNOWN_ADAPTERS.include?(normalized)
26
+
27
+ raise ArgumentError,
28
+ "unknown transaction adapter: #{adapter_name.inspect}. " \
29
+ "Known: #{KNOWN_ADAPTERS.map(&:inspect).join(", ")}"
30
+ end
31
+
19
32
  def self.detect
20
- if defined?(ActiveRecord::Base)
21
- ActiveRecordAdapter
22
- elsif defined?(Mongoid)
23
- MongoidAdapter
24
- end
33
+ return unless defined?(ActiveRecord::Base)
34
+ return unless active_record_pool?
35
+
36
+ ActiveRecordAdapter
37
+ end
38
+
39
+ def self.active_record_pool?
40
+ !ActiveRecord::Base.connection_handler.retrieve_connection_pool(
41
+ ActiveRecord::Base.connection_specification_name,
42
+ strict: false
43
+ ).nil?
25
44
  end
26
45
 
27
46
  module ActiveRecordAdapter
@@ -44,64 +63,6 @@ module Dex
44
63
  ActiveRecord::Rollback
45
64
  end
46
65
  end
47
-
48
- module MongoidAdapter
49
- AFTER_COMMIT_KEY = :_dex_mongoid_after_commit
50
-
51
- def self.wrap(&block)
52
- unless defined?(Mongoid)
53
- raise LoadError, "Mongoid is required for transactions"
54
- end
55
-
56
- callbacks = Thread.current[AFTER_COMMIT_KEY]
57
- outermost = callbacks.nil?
58
-
59
- unless outermost
60
- snapshot = callbacks.length
61
- begin
62
- return block.call
63
- rescue rollback_exception_class
64
- callbacks.slice!(snapshot..)
65
- return nil
66
- rescue StandardError # rubocop:disable Style/RescueStandardError
67
- callbacks.slice!(snapshot..)
68
- raise
69
- end
70
- end
71
-
72
- Thread.current[AFTER_COMMIT_KEY] = []
73
- block_completed = false
74
- result = Mongoid.transaction do
75
- value = block.call
76
- block_completed = true
77
- value
78
- end
79
-
80
- if block_completed
81
- Thread.current[AFTER_COMMIT_KEY].each(&:call)
82
- end
83
-
84
- result
85
- ensure
86
- Thread.current[AFTER_COMMIT_KEY] = nil if outermost
87
- end
88
-
89
- # NOTE: Only detects transactions opened via MongoidAdapter.wrap (i.e. Dex operations).
90
- # Ambient Mongoid.transaction blocks opened outside Dex are invisible here —
91
- # the callback will fire immediately instead of deferring to the outer commit.
92
- def self.after_commit(&block)
93
- callbacks = Thread.current[AFTER_COMMIT_KEY]
94
- if callbacks
95
- callbacks << block
96
- else
97
- block.call
98
- end
99
- end
100
-
101
- def self.rollback_exception_class
102
- Mongoid::Errors::Rollback
103
- end
104
- end
105
66
  end
106
67
  end
107
68
  end