dexkit 0.8.0 → 0.10.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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +36 -0
  3. data/README.md +50 -18
  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 +41 -23
  7. data/guides/llm/FORM.md +202 -61
  8. data/guides/llm/OPERATION.md +49 -20
  9. data/guides/llm/QUERY.md +52 -2
  10. data/lib/dex/context_dsl.rb +56 -0
  11. data/lib/dex/context_setup.rb +2 -33
  12. data/lib/dex/event/bus.rb +85 -8
  13. data/lib/dex/event/handler.rb +18 -0
  14. data/lib/dex/event/metadata.rb +16 -9
  15. data/lib/dex/event/processor.rb +1 -1
  16. data/lib/dex/event/test_helpers.rb +88 -0
  17. data/lib/dex/event/trace.rb +14 -27
  18. data/lib/dex/event.rb +2 -7
  19. data/lib/dex/event_test_helpers.rb +1 -86
  20. data/lib/dex/form/context.rb +27 -0
  21. data/lib/dex/form/export.rb +128 -0
  22. data/lib/dex/form/nesting.rb +2 -0
  23. data/lib/dex/form/uniqueness_validator.rb +17 -1
  24. data/lib/dex/form.rb +119 -3
  25. data/lib/dex/id.rb +38 -0
  26. data/lib/dex/operation/async_proxy.rb +13 -2
  27. data/lib/dex/operation/explain.rb +11 -7
  28. data/lib/dex/operation/jobs.rb +5 -4
  29. data/lib/dex/operation/lock_wrapper.rb +15 -2
  30. data/lib/dex/operation/once_wrapper.rb +24 -15
  31. data/lib/dex/operation/record_backend.rb +15 -1
  32. data/lib/dex/operation/record_wrapper.rb +43 -8
  33. data/lib/dex/operation/test_helpers/assertions.rb +359 -0
  34. data/lib/dex/operation/test_helpers/execution.rb +30 -0
  35. data/lib/dex/operation/test_helpers/stubbing.rb +61 -0
  36. data/lib/dex/operation/test_helpers.rb +160 -0
  37. data/lib/dex/operation/trace_wrapper.rb +20 -0
  38. data/lib/dex/operation/transaction_adapter.rb +29 -68
  39. data/lib/dex/operation/transaction_wrapper.rb +10 -16
  40. data/lib/dex/operation.rb +2 -0
  41. data/lib/dex/query/backend.rb +13 -0
  42. data/lib/dex/query/export.rb +64 -0
  43. data/lib/dex/query.rb +50 -5
  44. data/lib/dex/ref_type.rb +4 -0
  45. data/lib/dex/test_helpers.rb +4 -139
  46. data/lib/dex/test_log.rb +62 -4
  47. data/lib/dex/trace.rb +291 -0
  48. data/lib/dex/type_coercion.rb +4 -1
  49. data/lib/dex/version.rb +1 -1
  50. data/lib/dexkit.rb +9 -5
  51. metadata +16 -5
  52. data/lib/dex/test_helpers/assertions.rb +0 -333
  53. data/lib/dex/test_helpers/execution.rb +0 -28
  54. data/lib/dex/test_helpers/stubbing.rb +0 -59
  55. /data/lib/dex/{event_test_helpers → event/test_helpers}/assertions.rb +0 -0
@@ -0,0 +1,160 @@
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
+ trace = Dex::Trace.current + [{
101
+ type: :operation,
102
+ id: @_dex_execution_id,
103
+ class: self.class.name || self.class.to_s
104
+ }]
105
+
106
+ entry = Dex::TestLog::Entry.new(
107
+ type: "Operation",
108
+ name: self.class.name || self.class.to_s,
109
+ operation_class: self.class,
110
+ params: _test_safe_params,
111
+ result: safe_result,
112
+ duration: duration,
113
+ caller_location: caller_locations(4, 1)&.first,
114
+ execution_id: @_dex_execution_id,
115
+ trace_id: @_dex_trace_id,
116
+ trace: trace
117
+ )
118
+ Dex::TestLog.record(entry)
119
+ end
120
+ end
121
+
122
+ module TestHelpers
123
+ extend Dex::Concern
124
+
125
+ def self.included(base)
126
+ Dex::Operation::TestWrapper.install!
127
+ super
128
+ end
129
+
130
+ def setup
131
+ super
132
+ Dex::Trace.clear!
133
+ Dex::TestLog.clear!
134
+ Dex::Operation::TestWrapper.clear_all_stubs!
135
+ end
136
+
137
+ module ClassMethods
138
+ def testing(klass)
139
+ @_dex_test_subject = klass
140
+ end
141
+
142
+ def _dex_test_subject
143
+ return @_dex_test_subject if defined?(@_dex_test_subject) && @_dex_test_subject
144
+
145
+ superclass._dex_test_subject if superclass.respond_to?(:_dex_test_subject)
146
+ end
147
+ end
148
+
149
+ private
150
+
151
+ def _dex_test_subject
152
+ self.class._dex_test_subject
153
+ end
154
+ end
155
+ end
156
+ end
157
+
158
+ require_relative "test_helpers/execution"
159
+ require_relative "test_helpers/assertions"
160
+ require_relative "test_helpers/stubbing"
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dex
4
+ module TraceWrapper
5
+ extend Dex::Concern
6
+
7
+ def _trace_wrap
8
+ @_dex_execution_id ||= Dex::Id.generate("op_")
9
+
10
+ Dex::Trace.with_frame(
11
+ type: :operation,
12
+ id: @_dex_execution_id,
13
+ class: self.class.name
14
+ ) do
15
+ @_dex_trace_id = Dex::Trace.trace_id
16
+ yield
17
+ end
18
+ end
19
+ end
20
+ end
@@ -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
@@ -7,10 +7,10 @@ module Dex
7
7
  DEFERRED_CALLBACKS_KEY = :_dex_after_commit_queue
8
8
 
9
9
  def _transaction_wrap
10
- deferred = Thread.current[DEFERRED_CALLBACKS_KEY]
10
+ deferred = Fiber[DEFERRED_CALLBACKS_KEY]
11
11
  outermost = deferred.nil?
12
- Thread.current[DEFERRED_CALLBACKS_KEY] = [] if outermost
13
- snapshot = Thread.current[DEFERRED_CALLBACKS_KEY].length
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
- Thread.current[DEFERRED_CALLBACKS_KEY]&.slice!(snapshot..)
26
+ Fiber[DEFERRED_CALLBACKS_KEY]&.slice!(snapshot..)
27
27
  raise
28
28
  ensure
29
- Thread.current[DEFERRED_CALLBACKS_KEY] = nil if outermost
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 = Thread.current[DEFERRED_CALLBACKS_KEY]
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
- unless TransactionWrapper::TRANSACTION_KNOWN_ADAPTERS.include?(adapter.to_sym)
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
- Thread.current[DEFERRED_CALLBACKS_KEY]&.slice!(snapshot..)
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
- Thread.current[DEFERRED_CALLBACKS_KEY]&.slice!(snapshot..)
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 = Thread.current[DEFERRED_CALLBACKS_KEY]
114
+ callbacks = Fiber[DEFERRED_CALLBACKS_KEY]
121
115
  return if callbacks.empty?
122
116
 
123
117
  flush = -> { callbacks.each(&:call) }
data/lib/dex/operation.rb CHANGED
@@ -4,6 +4,7 @@
4
4
  require_relative "operation/result_wrapper"
5
5
  require_relative "operation/once_wrapper"
6
6
  require_relative "operation/record_wrapper"
7
+ require_relative "operation/trace_wrapper"
7
8
  require_relative "operation/transaction_wrapper"
8
9
  require_relative "operation/lock_wrapper"
9
10
  require_relative "operation/async_wrapper"
@@ -139,6 +140,7 @@ module Dex
139
140
  include AsyncWrapper
140
141
  include SafeWrapper
141
142
 
143
+ use TraceWrapper, at: :outer
142
144
  use ResultWrapper
143
145
  use GuardWrapper
144
146
  use OnceWrapper
@@ -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
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dex
4
+ class Query
5
+ module Export
6
+ module_function
7
+
8
+ def build_hash(source)
9
+ h = {}
10
+ h[:name] = source.name if source.name
11
+ desc = source.description
12
+ h[:description] = desc if desc
13
+ h[:props] = _serialize_props(source)
14
+ ctx = _serialize_context(source)
15
+ h[:context] = ctx unless ctx.empty?
16
+ h[:filters] = source.filters unless source.filters.empty?
17
+ h[:sorts] = source.sorts unless source.sorts.empty?
18
+ h
19
+ end
20
+
21
+ def build_json_schema(source) # rubocop:disable Metrics/MethodLength
22
+ descs = source.respond_to?(:prop_descriptions) ? source.prop_descriptions : {}
23
+ properties = {}
24
+ required = []
25
+
26
+ if source.respond_to?(:literal_properties)
27
+ source.literal_properties.each do |prop|
28
+ prop_desc = descs[prop.name]
29
+ schema = TypeSerializer.to_json_schema(prop.type, desc: prop_desc)
30
+ properties[prop.name.to_s] = schema
31
+ required << prop.name.to_s if prop.required?
32
+ end
33
+ end
34
+
35
+ result = { "$schema": "https://json-schema.org/draft/2020-12/schema" }
36
+ result[:title] = source.name if source.name
37
+ desc = source.description
38
+ result[:description] = desc if desc
39
+ result[:type] = "object"
40
+ result[:properties] = properties unless properties.empty?
41
+ result[:required] = required unless required.empty?
42
+ result[:additionalProperties] = false
43
+ result
44
+ end
45
+
46
+ def _serialize_props(source)
47
+ return {} unless source.respond_to?(:literal_properties)
48
+
49
+ descs = source.respond_to?(:prop_descriptions) ? source.prop_descriptions : {}
50
+ source.literal_properties.each_with_object({}) do |prop, hash|
51
+ entry = { type: TypeSerializer.to_string(prop.type), required: prop.required? }
52
+ entry[:desc] = descs[prop.name] if descs[prop.name]
53
+ hash[prop.name] = entry
54
+ end
55
+ end
56
+
57
+ def _serialize_context(source)
58
+ source.respond_to?(:context_mappings) ? source.context_mappings.presence || {} : {}
59
+ end
60
+
61
+ private_class_method :_serialize_props, :_serialize_context
62
+ end
63
+ end
64
+ end
data/lib/dex/query.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require "active_model"
4
4
 
5
5
  require_relative "query/backend"
6
+ require_relative "query/export"
6
7
  require_relative "query/filtering"
7
8
  require_relative "query/sorting"
8
9
 
@@ -17,7 +18,11 @@ module Dex
17
18
  extend ActiveModel::Naming
18
19
  include ActiveModel::Conversion
19
20
 
21
+ extend Registry
22
+
20
23
  class << self
24
+ include ContextDSL
25
+
21
26
  def scope(&block)
22
27
  raise ArgumentError, "scope requires a block." unless block
23
28
 
@@ -31,6 +36,14 @@ module Dex
31
36
  end
32
37
 
33
38
  def new(scope: nil, sort: nil, **kwargs)
39
+ mappings = context_mappings
40
+ unless mappings.empty?
41
+ ambient = Dex.context
42
+ mappings.each do |prop_name, context_key|
43
+ next if kwargs.key?(prop_name)
44
+ kwargs[prop_name] = ambient[context_key] if ambient.key?(context_key)
45
+ end
46
+ end
34
47
  instance = super(**kwargs)
35
48
  instance.instance_variable_set(:@_injected_scope, scope)
36
49
  sort_str = sort&.to_s
@@ -96,6 +109,30 @@ module Dex
96
109
  subclass.instance_variable_set(:@_sort_default, _sort_default) if _sort_default
97
110
  end
98
111
 
112
+ # Export
113
+
114
+ def to_h
115
+ Export.build_hash(self)
116
+ end
117
+
118
+ def to_json_schema
119
+ Export.build_json_schema(self)
120
+ end
121
+
122
+ def export(format: :hash)
123
+ unless %i[hash json_schema].include?(format)
124
+ raise ArgumentError, "unknown format: #{format.inspect}. Known: :hash, :json_schema"
125
+ end
126
+
127
+ sorted = registry.sort_by(&:name)
128
+ sorted.map do |klass|
129
+ case format
130
+ when :hash then klass.to_h
131
+ when :json_schema then klass.to_json_schema
132
+ end
133
+ end
134
+ end
135
+
99
136
  def from_params(params, scope: nil, **overrides)
100
137
  pk = model_name.param_key
101
138
  nested = _extract_nested_params(params, pk)
@@ -139,6 +176,10 @@ module Dex
139
176
 
140
177
  private
141
178
 
179
+ def _context_prop_declared?(name)
180
+ respond_to?(:literal_properties) && literal_properties.any? { |p| p.name == name }
181
+ end
182
+
142
183
  def _extract_nested_params(params, pk)
143
184
  hash = if params.respond_to?(:to_unsafe_h)
144
185
  params.to_unsafe_h
@@ -209,9 +250,10 @@ module Dex
209
250
  end
210
251
 
211
252
  def resolve
212
- base = _evaluate_scope
253
+ base = Query::Backend.normalize_scope(_evaluate_scope)
213
254
  base = _merge_injected_scope(base)
214
255
  base = _apply_filters(base)
256
+ base = Query::Backend.normalize_scope(base)
215
257
  _apply_sort(base)
216
258
  end
217
259
 
@@ -253,19 +295,22 @@ module Dex
253
295
  def _merge_injected_scope(base)
254
296
  return base unless @_injected_scope
255
297
 
298
+ base = Query::Backend.normalize_scope(base)
299
+ injected_scope = Query::Backend.normalize_scope(@_injected_scope)
300
+
256
301
  unless base.respond_to?(:klass)
257
302
  raise ArgumentError, "Scope block must return a queryable scope (ActiveRecord relation or Mongoid criteria), got #{base.class}."
258
303
  end
259
304
 
260
- unless @_injected_scope.respond_to?(:klass)
305
+ unless injected_scope.respond_to?(:klass)
261
306
  raise ArgumentError, "Injected scope must be a queryable scope (ActiveRecord relation or Mongoid criteria), got #{@_injected_scope.class}."
262
307
  end
263
308
 
264
- unless base.klass == @_injected_scope.klass
265
- raise ArgumentError, "Scope model mismatch: expected #{base.klass}, got #{@_injected_scope.klass}."
309
+ unless base.klass == injected_scope.klass
310
+ raise ArgumentError, "Scope model mismatch: expected #{base.klass}, got #{injected_scope.klass}."
266
311
  end
267
312
 
268
- base.merge(@_injected_scope)
313
+ base.merge(injected_scope)
269
314
  end
270
315
  end
271
316
  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