dexkit 0.8.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +14 -0
- data/README.md +6 -2
- data/gemfiles/mongoid_no_ar.gemfile +10 -0
- data/gemfiles/mongoid_no_ar.gemfile.lock +232 -0
- data/guides/llm/EVENT.md +17 -4
- data/guides/llm/FORM.md +2 -2
- data/guides/llm/OPERATION.md +22 -17
- data/guides/llm/QUERY.md +2 -2
- data/lib/dex/event/bus.rb +7 -0
- data/lib/dex/event/test_helpers.rb +88 -0
- data/lib/dex/event_test_helpers.rb +1 -86
- data/lib/dex/form/uniqueness_validator.rb +17 -1
- data/lib/dex/operation/async_proxy.rb +1 -0
- data/lib/dex/operation/explain.rb +11 -7
- data/lib/dex/operation/lock_wrapper.rb +15 -2
- data/lib/dex/operation/once_wrapper.rb +23 -15
- data/lib/dex/operation/record_backend.rb +13 -0
- data/lib/dex/operation/record_wrapper.rb +29 -4
- data/lib/dex/operation/test_helpers/assertions.rb +335 -0
- data/lib/dex/operation/test_helpers/execution.rb +30 -0
- data/lib/dex/operation/test_helpers/stubbing.rb +61 -0
- data/lib/dex/operation/test_helpers.rb +150 -0
- data/lib/dex/operation/transaction_adapter.rb +29 -68
- data/lib/dex/operation/transaction_wrapper.rb +10 -16
- data/lib/dex/query/backend.rb +13 -0
- data/lib/dex/query.rb +9 -5
- data/lib/dex/ref_type.rb +4 -0
- data/lib/dex/test_helpers.rb +4 -139
- data/lib/dex/type_coercion.rb +4 -1
- data/lib/dex/version.rb +1 -1
- data/lib/dexkit.rb +6 -5
- metadata +9 -5
- data/lib/dex/test_helpers/assertions.rb +0 -333
- data/lib/dex/test_helpers/execution.rb +0 -28
- data/lib/dex/test_helpers/stubbing.rb +0 -59
- /data/lib/dex/{event_test_helpers → event/test_helpers}/assertions.rb +0 -0
|
@@ -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
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
@@ -7,10 +7,10 @@ module Dex
|
|
|
7
7
|
DEFERRED_CALLBACKS_KEY = :_dex_after_commit_queue
|
|
8
8
|
|
|
9
9
|
def _transaction_wrap
|
|
10
|
-
deferred =
|
|
10
|
+
deferred = Fiber[DEFERRED_CALLBACKS_KEY]
|
|
11
11
|
outermost = deferred.nil?
|
|
12
|
-
|
|
13
|
-
snapshot =
|
|
12
|
+
Fiber[DEFERRED_CALLBACKS_KEY] = [] if outermost
|
|
13
|
+
snapshot = Fiber[DEFERRED_CALLBACKS_KEY].length
|
|
14
14
|
|
|
15
15
|
result, interceptor = if _transaction_enabled?
|
|
16
16
|
_transaction_run_adapter(snapshot) { yield }
|
|
@@ -23,16 +23,16 @@ module Dex
|
|
|
23
23
|
interceptor&.rethrow!
|
|
24
24
|
result
|
|
25
25
|
rescue # rubocop:disable Style/RescueStandardError -- explicit for clarity
|
|
26
|
-
|
|
26
|
+
Fiber[DEFERRED_CALLBACKS_KEY]&.slice!(snapshot..)
|
|
27
27
|
raise
|
|
28
28
|
ensure
|
|
29
|
-
|
|
29
|
+
Fiber[DEFERRED_CALLBACKS_KEY] = nil if outermost
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def after_commit(&block)
|
|
33
33
|
raise ArgumentError, "after_commit requires a block" unless block
|
|
34
34
|
|
|
35
|
-
deferred =
|
|
35
|
+
deferred = Fiber[DEFERRED_CALLBACKS_KEY]
|
|
36
36
|
if deferred
|
|
37
37
|
deferred << block
|
|
38
38
|
else
|
|
@@ -40,8 +40,6 @@ module Dex
|
|
|
40
40
|
end
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
TRANSACTION_KNOWN_ADAPTERS = %i[active_record mongoid].freeze
|
|
44
|
-
|
|
45
43
|
module ClassMethods
|
|
46
44
|
def transaction(enabled_or_options = nil, **options)
|
|
47
45
|
validate_options!(options, %i[adapter], :transaction)
|
|
@@ -66,11 +64,7 @@ module Dex
|
|
|
66
64
|
def _transaction_validate_adapter!(adapter)
|
|
67
65
|
return if adapter.nil?
|
|
68
66
|
|
|
69
|
-
|
|
70
|
-
raise ArgumentError,
|
|
71
|
-
"unknown transaction adapter: #{adapter.inspect}. " \
|
|
72
|
-
"Known: #{TransactionWrapper::TRANSACTION_KNOWN_ADAPTERS.map(&:inspect).join(", ")}"
|
|
73
|
-
end
|
|
67
|
+
Operation::TransactionAdapter.normalize_name(adapter)
|
|
74
68
|
end
|
|
75
69
|
end
|
|
76
70
|
|
|
@@ -85,7 +79,7 @@ module Dex
|
|
|
85
79
|
end
|
|
86
80
|
|
|
87
81
|
if interceptor&.error?
|
|
88
|
-
|
|
82
|
+
Fiber[DEFERRED_CALLBACKS_KEY]&.slice!(snapshot..)
|
|
89
83
|
interceptor.rethrow!
|
|
90
84
|
end
|
|
91
85
|
|
|
@@ -96,7 +90,7 @@ module Dex
|
|
|
96
90
|
interceptor = Operation::HaltInterceptor.new { yield }
|
|
97
91
|
|
|
98
92
|
if interceptor.error?
|
|
99
|
-
|
|
93
|
+
Fiber[DEFERRED_CALLBACKS_KEY]&.slice!(snapshot..)
|
|
100
94
|
interceptor.rethrow!
|
|
101
95
|
end
|
|
102
96
|
|
|
@@ -117,7 +111,7 @@ module Dex
|
|
|
117
111
|
end
|
|
118
112
|
|
|
119
113
|
def _transaction_flush_deferred
|
|
120
|
-
callbacks =
|
|
114
|
+
callbacks = Fiber[DEFERRED_CALLBACKS_KEY]
|
|
121
115
|
return if callbacks.empty?
|
|
122
116
|
|
|
123
117
|
flush = -> { callbacks.each(&:call) }
|
data/lib/dex/query/backend.rb
CHANGED
|
@@ -76,16 +76,29 @@ module Dex
|
|
|
76
76
|
module_function
|
|
77
77
|
|
|
78
78
|
def apply_strategy(scope, strategy, column, value)
|
|
79
|
+
scope = normalize_scope(scope)
|
|
79
80
|
adapter_for(scope).apply(scope, strategy, column, value)
|
|
80
81
|
end
|
|
81
82
|
|
|
82
83
|
def adapter_for(scope)
|
|
84
|
+
scope = normalize_scope(scope)
|
|
85
|
+
|
|
83
86
|
if defined?(Mongoid::Criteria) && scope.is_a?(Mongoid::Criteria)
|
|
84
87
|
MongoidAdapter
|
|
85
88
|
else
|
|
86
89
|
ActiveRecordAdapter
|
|
87
90
|
end
|
|
88
91
|
end
|
|
92
|
+
|
|
93
|
+
def normalize_scope(scope)
|
|
94
|
+
return scope unless defined?(Mongoid::Criteria)
|
|
95
|
+
return scope if scope.is_a?(Mongoid::Criteria)
|
|
96
|
+
|
|
97
|
+
criteria = scope.criteria if scope.respond_to?(:criteria)
|
|
98
|
+
criteria.is_a?(Mongoid::Criteria) ? criteria : scope
|
|
99
|
+
rescue
|
|
100
|
+
scope
|
|
101
|
+
end
|
|
89
102
|
end
|
|
90
103
|
end
|
|
91
104
|
end
|
data/lib/dex/query.rb
CHANGED
|
@@ -209,9 +209,10 @@ module Dex
|
|
|
209
209
|
end
|
|
210
210
|
|
|
211
211
|
def resolve
|
|
212
|
-
base = _evaluate_scope
|
|
212
|
+
base = Query::Backend.normalize_scope(_evaluate_scope)
|
|
213
213
|
base = _merge_injected_scope(base)
|
|
214
214
|
base = _apply_filters(base)
|
|
215
|
+
base = Query::Backend.normalize_scope(base)
|
|
215
216
|
_apply_sort(base)
|
|
216
217
|
end
|
|
217
218
|
|
|
@@ -253,19 +254,22 @@ module Dex
|
|
|
253
254
|
def _merge_injected_scope(base)
|
|
254
255
|
return base unless @_injected_scope
|
|
255
256
|
|
|
257
|
+
base = Query::Backend.normalize_scope(base)
|
|
258
|
+
injected_scope = Query::Backend.normalize_scope(@_injected_scope)
|
|
259
|
+
|
|
256
260
|
unless base.respond_to?(:klass)
|
|
257
261
|
raise ArgumentError, "Scope block must return a queryable scope (ActiveRecord relation or Mongoid criteria), got #{base.class}."
|
|
258
262
|
end
|
|
259
263
|
|
|
260
|
-
unless
|
|
264
|
+
unless injected_scope.respond_to?(:klass)
|
|
261
265
|
raise ArgumentError, "Injected scope must be a queryable scope (ActiveRecord relation or Mongoid criteria), got #{@_injected_scope.class}."
|
|
262
266
|
end
|
|
263
267
|
|
|
264
|
-
unless base.klass ==
|
|
265
|
-
raise ArgumentError, "Scope model mismatch: expected #{base.klass}, got #{
|
|
268
|
+
unless base.klass == injected_scope.klass
|
|
269
|
+
raise ArgumentError, "Scope model mismatch: expected #{base.klass}, got #{injected_scope.klass}."
|
|
266
270
|
end
|
|
267
271
|
|
|
268
|
-
base.merge(
|
|
272
|
+
base.merge(injected_scope)
|
|
269
273
|
end
|
|
270
274
|
end
|
|
271
275
|
end
|
data/lib/dex/ref_type.rb
CHANGED
|
@@ -7,6 +7,10 @@ module Dex
|
|
|
7
7
|
attr_reader :model_class, :lock
|
|
8
8
|
|
|
9
9
|
def initialize(model_class, lock: false)
|
|
10
|
+
if lock && !model_class.respond_to?(:lock)
|
|
11
|
+
raise ArgumentError, "_Ref(lock: true) requires a model class that responds to .lock"
|
|
12
|
+
end
|
|
13
|
+
|
|
10
14
|
@model_class = model_class
|
|
11
15
|
@lock = lock
|
|
12
16
|
end
|
data/lib/dex/test_helpers.rb
CHANGED
|
@@ -1,148 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "
|
|
3
|
+
require_relative "operation/test_helpers"
|
|
4
|
+
require_relative "event/test_helpers"
|
|
4
5
|
|
|
5
6
|
module Dex
|
|
6
|
-
module TestWrapper
|
|
7
|
-
@_installed = false
|
|
8
|
-
|
|
9
|
-
class << self
|
|
10
|
-
def install!
|
|
11
|
-
return if @_installed
|
|
12
|
-
|
|
13
|
-
Dex::Operation.prepend(self)
|
|
14
|
-
@_installed = true
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def installed?
|
|
18
|
-
@_installed
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
# Stub registry
|
|
22
|
-
|
|
23
|
-
def stubs
|
|
24
|
-
@_stubs ||= {}
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def find_stub(klass)
|
|
28
|
-
stubs[klass]
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def register_stub(klass, **options)
|
|
32
|
-
stubs[klass] = options
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def clear_stub(klass)
|
|
36
|
-
stubs.delete(klass)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def clear_all_stubs!
|
|
40
|
-
stubs.clear
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def call
|
|
45
|
-
stub = Dex::TestWrapper.find_stub(self.class)
|
|
46
|
-
return _test_apply_stub(stub) if stub
|
|
47
|
-
|
|
48
|
-
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
49
|
-
result = nil
|
|
50
|
-
err = nil
|
|
51
|
-
|
|
52
|
-
begin
|
|
53
|
-
result = super
|
|
54
|
-
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
55
|
-
err = e
|
|
56
|
-
raise
|
|
57
|
-
ensure
|
|
58
|
-
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at
|
|
59
|
-
_test_record_to_log(result, err, duration)
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
result
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
private
|
|
66
|
-
|
|
67
|
-
def _test_apply_stub(stub)
|
|
68
|
-
if stub[:error]
|
|
69
|
-
err_opts = stub[:error]
|
|
70
|
-
case err_opts
|
|
71
|
-
when Symbol
|
|
72
|
-
raise Dex::Error.new(err_opts)
|
|
73
|
-
when Hash
|
|
74
|
-
raise Dex::Error.new(err_opts[:code], err_opts[:message], details: err_opts[:details])
|
|
75
|
-
end
|
|
76
|
-
else
|
|
77
|
-
stub[:returns]
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def _test_safe_params
|
|
82
|
-
respond_to?(:to_h) ? to_h : {}
|
|
83
|
-
rescue
|
|
84
|
-
{}
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def _test_record_to_log(result, err, duration)
|
|
88
|
-
safe_result = if err
|
|
89
|
-
dex_err = if err.is_a?(Dex::Error)
|
|
90
|
-
err
|
|
91
|
-
else
|
|
92
|
-
Dex::Error.new(:exception, err.message, details: { exception_class: err.class.name })
|
|
93
|
-
end
|
|
94
|
-
Dex::Operation::Err.new(dex_err)
|
|
95
|
-
else
|
|
96
|
-
Dex::Operation::Ok.new(result)
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
entry = Dex::TestLog::Entry.new(
|
|
100
|
-
type: "Operation",
|
|
101
|
-
name: self.class.name || self.class.to_s,
|
|
102
|
-
operation_class: self.class,
|
|
103
|
-
params: _test_safe_params,
|
|
104
|
-
result: safe_result,
|
|
105
|
-
duration: duration,
|
|
106
|
-
caller_location: caller_locations(4, 1)&.first
|
|
107
|
-
)
|
|
108
|
-
Dex::TestLog.record(entry)
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
|
|
112
7
|
module TestHelpers
|
|
113
|
-
extend Dex::Concern
|
|
114
|
-
|
|
115
8
|
def self.included(base)
|
|
116
|
-
Dex::
|
|
117
|
-
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def setup
|
|
121
|
-
super
|
|
122
|
-
Dex::TestLog.clear!
|
|
123
|
-
Dex::TestWrapper.clear_all_stubs!
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
module ClassMethods
|
|
127
|
-
def testing(klass)
|
|
128
|
-
@_dex_test_subject = klass
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
def _dex_test_subject
|
|
132
|
-
return @_dex_test_subject if defined?(@_dex_test_subject) && @_dex_test_subject
|
|
133
|
-
|
|
134
|
-
superclass._dex_test_subject if superclass.respond_to?(:_dex_test_subject)
|
|
135
|
-
end
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
private
|
|
139
|
-
|
|
140
|
-
def _dex_test_subject
|
|
141
|
-
self.class._dex_test_subject
|
|
9
|
+
base.include(Dex::Operation::TestHelpers)
|
|
10
|
+
base.include(Dex::Event::TestHelpers)
|
|
142
11
|
end
|
|
143
12
|
end
|
|
144
13
|
end
|
|
145
|
-
|
|
146
|
-
require_relative "test_helpers/execution"
|
|
147
|
-
require_relative "test_helpers/assertions"
|
|
148
|
-
require_relative "test_helpers/stubbing"
|
data/lib/dex/type_coercion.rb
CHANGED
|
@@ -64,7 +64,10 @@ module Dex
|
|
|
64
64
|
return _serialize_value(type.type, value) if type.is_a?(Literal::Types::NilableType)
|
|
65
65
|
|
|
66
66
|
ref = _find_ref_type(type)
|
|
67
|
-
|
|
67
|
+
if ref
|
|
68
|
+
serialized_id = value.id
|
|
69
|
+
return serialized_id.respond_to?(:as_json) ? serialized_id.as_json : serialized_id
|
|
70
|
+
end
|
|
68
71
|
|
|
69
72
|
value.respond_to?(:as_json) ? value.as_json : value
|
|
70
73
|
end
|
data/lib/dex/version.rb
CHANGED