dexkit 0.6.0 → 0.8.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.
@@ -0,0 +1,240 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dex
4
+ module OnceWrapper
5
+ extend Dex::Concern
6
+
7
+ module ClassMethods
8
+ def once(*props, expires_in: nil, &block)
9
+ if settings_for(:once)[:defined]
10
+ raise ArgumentError, "once can only be declared once per operation"
11
+ end
12
+
13
+ record_settings = settings_for(:record)
14
+ if record_settings[:enabled] == false
15
+ raise ArgumentError, "once requires record to be enabled"
16
+ end
17
+
18
+ if record_settings[:result] == false
19
+ raise ArgumentError, "once requires result recording (cannot use record result: false)"
20
+ end
21
+
22
+ if block && props.any?
23
+ raise ArgumentError, "once accepts either prop names or a block, not both"
24
+ end
25
+
26
+ if expires_in && !expires_in.is_a?(Numeric)
27
+ raise ArgumentError, "once :expires_in must be a duration, got: #{expires_in.inspect}"
28
+ end
29
+
30
+ _once_validate_props!(props) if props.any?
31
+
32
+ set(:once,
33
+ defined: true,
34
+ props: props.any? ? props : nil,
35
+ block: block || nil,
36
+ expires_in: expires_in)
37
+ end
38
+
39
+ def clear_once!(key = nil, **props)
40
+ derived = if key.is_a?(String)
41
+ key
42
+ elsif props.any?
43
+ _once_build_scoped_key(props)
44
+ else
45
+ raise ArgumentError, "pass a String key or keyword arguments matching the once props"
46
+ end
47
+ Dex.record_backend.update_record_by_once_key(derived, once_key: nil)
48
+ end
49
+
50
+ def _once_build_scoped_key(props_hash)
51
+ segments = props_hash.sort_by { |k, _| k.to_s }.map do |k, v|
52
+ "#{k}=#{URI.encode_www_form_component(v.to_s)}"
53
+ end
54
+ "#{name}/#{segments.join("/")}"
55
+ end
56
+
57
+ private
58
+
59
+ def _once_validate_props!(prop_names)
60
+ return unless respond_to?(:literal_properties)
61
+
62
+ defined_names = literal_properties.map(&:name).to_set
63
+ unknown = prop_names.reject { |p| defined_names.include?(p) }
64
+ return if unknown.empty?
65
+
66
+ raise ArgumentError,
67
+ "once references unknown prop(s): #{unknown.map(&:inspect).join(", ")}. " \
68
+ "Defined: #{defined_names.map(&:inspect).join(", ")}"
69
+ end
70
+ end
71
+
72
+ def once(key)
73
+ @_once_key = key
74
+ @_once_key_explicit = true
75
+ self
76
+ end
77
+
78
+ def _once_wrap
79
+ return yield unless _once_active?
80
+
81
+ key = _once_derive_key
82
+ return yield if key.nil? && _once_key_explicit?
83
+
84
+ _once_ensure_backend!
85
+
86
+ raise "once key must not be nil" if key.nil?
87
+
88
+ expires_in = self.class.settings_for(:once)[:expires_in]
89
+ expires_at = expires_in ? _once_current_time + expires_in : nil
90
+
91
+ existing = Dex.record_backend.find_by_once_key(key)
92
+ if existing
93
+ _once_finalize_duplicate!(existing)
94
+ return _once_replay!(existing)
95
+ end
96
+
97
+ expired = Dex.record_backend.find_expired_once_key(key)
98
+ Dex.record_backend.update_record(expired.id.to_s, once_key: nil) if expired
99
+
100
+ _once_claim!(key, expires_at) { yield }
101
+ end
102
+
103
+ private
104
+
105
+ def _once_key_explicit?
106
+ defined?(@_once_key_explicit) && @_once_key_explicit
107
+ end
108
+
109
+ def _once_active?
110
+ return true if _once_key_explicit?
111
+ self.class.settings_for(:once).fetch(:defined, false)
112
+ end
113
+
114
+ def _once_ensure_backend!
115
+ unless Dex.record_backend
116
+ raise "once requires a record backend (configure Dex.record_class)"
117
+ end
118
+
119
+ return if self.class.instance_variable_defined?(:@_once_fields_checked)
120
+
121
+ unless Dex.record_backend.has_field?("once_key")
122
+ raise "once requires once_key column on #{Dex.record_class}. Run the migration to add it."
123
+ end
124
+
125
+ if self.class.settings_for(:once)[:expires_in] && !Dex.record_backend.has_field?("once_key_expires_at")
126
+ raise "once with expires_in requires once_key_expires_at column on #{Dex.record_class}. Run the migration to add it."
127
+ end
128
+
129
+ self.class.instance_variable_set(:@_once_fields_checked, true)
130
+ end
131
+
132
+ def _once_derive_key
133
+ return @_once_key if _once_key_explicit?
134
+
135
+ settings = self.class.settings_for(:once)
136
+
137
+ if settings[:block]
138
+ instance_exec(&settings[:block])
139
+ elsif settings[:props]
140
+ props_hash = settings[:props].each_with_object({}) { |p, h| h[p] = public_send(p) }
141
+ self.class._once_build_scoped_key(props_hash)
142
+ else
143
+ hash = {}
144
+ if self.class.respond_to?(:literal_properties)
145
+ self.class.literal_properties.each { |p| hash[p.name] = public_send(p.name) }
146
+ end
147
+ self.class._once_build_scoped_key(hash)
148
+ end
149
+ end
150
+
151
+ def _once_claim!(key, expires_at)
152
+ begin
153
+ _once_acquire_key!(key, expires_at)
154
+ rescue => e
155
+ if Dex.record_backend.unique_constraint_error?(e)
156
+ existing = Dex.record_backend.find_by_once_key(key)
157
+ if existing
158
+ _once_finalize_duplicate!(existing)
159
+ return _once_replay!(existing)
160
+ end
161
+
162
+ raise "once key #{key.inspect} is claimed by another in-flight execution"
163
+ end
164
+ raise
165
+ end
166
+ yield
167
+ end
168
+
169
+ def _once_acquire_key!(key, expires_at)
170
+ if _once_has_pending_record?
171
+ Dex.record_backend.update_record(@_dex_record_id,
172
+ once_key: key, once_key_expires_at: expires_at)
173
+ else
174
+ record = Dex.record_backend.create_record(
175
+ name: self.class.name,
176
+ once_key: key,
177
+ once_key_expires_at: expires_at,
178
+ status: "pending"
179
+ )
180
+ @_dex_record_id = record.id.to_s
181
+ end
182
+ end
183
+
184
+ def _once_has_pending_record?
185
+ defined?(@_dex_record_id) && @_dex_record_id
186
+ end
187
+
188
+ def _once_finalize_duplicate!(source_record)
189
+ return unless _once_has_pending_record?
190
+
191
+ attrs = { performed_at: _once_current_time }
192
+ if source_record.status == "error"
193
+ attrs[:status] = "error"
194
+ attrs[:error_code] = source_record.error_code
195
+ attrs[:error_message] = source_record.error_message
196
+ attrs[:error_details] = source_record.respond_to?(:error_details) ? source_record.error_details : nil
197
+ else
198
+ attrs[:status] = "completed"
199
+ attrs[:result] = source_record.respond_to?(:result) ? source_record.result : nil
200
+ end
201
+ Dex.record_backend.update_record(@_dex_record_id, attrs)
202
+ rescue => e
203
+ Dex.warn("Failed to finalize replayed record: #{e.message}")
204
+ end
205
+
206
+ def _once_replay!(record)
207
+ case record.status
208
+ when "completed"
209
+ _once_replay_success(record)
210
+ when "error"
211
+ _once_replay_error(record)
212
+ end
213
+ end
214
+
215
+ def _once_replay_success(record)
216
+ stored = record.respond_to?(:result) ? record.result : nil
217
+ success_type = self.class.respond_to?(:_success_type) && self.class._success_type
218
+
219
+ if success_type && stored
220
+ self.class.send(:_coerce_value, success_type, stored)
221
+ elsif stored.is_a?(Hash) && stored.key?("_dex_value")
222
+ stored["_dex_value"]
223
+ else
224
+ stored
225
+ end
226
+ end
227
+
228
+ def _once_replay_error(record)
229
+ raise Dex::Error.new(
230
+ record.error_code.to_sym,
231
+ record.error_message,
232
+ details: record.respond_to?(:error_details) ? record.error_details : nil
233
+ )
234
+ end
235
+
236
+ def _once_current_time
237
+ Time.respond_to?(:current) ? Time.current : Time.now
238
+ end
239
+ end
240
+ end
@@ -34,6 +34,26 @@ module Dex
34
34
  record_class.find(id).update!(safe_attributes(attributes))
35
35
  end
36
36
 
37
+ def find_by_once_key(key)
38
+ raise NotImplementedError
39
+ end
40
+
41
+ def find_expired_once_key(key)
42
+ raise NotImplementedError
43
+ end
44
+
45
+ def find_pending_once_key(key)
46
+ raise NotImplementedError
47
+ end
48
+
49
+ def update_record_by_once_key(key, **attributes)
50
+ raise NotImplementedError
51
+ end
52
+
53
+ def unique_constraint_error?(exception)
54
+ raise NotImplementedError
55
+ end
56
+
37
57
  def safe_attributes(attributes)
38
58
  attributes.select { |key, _| has_field?(key.to_s) }
39
59
  end
@@ -49,12 +69,79 @@ module Dex
49
69
  @column_set = record_class.column_names.to_set
50
70
  end
51
71
 
72
+ def find_by_once_key(key)
73
+ scope = record_class.where(once_key: key, status: %w[completed error])
74
+ scope = scope.where("once_key_expires_at IS NULL OR once_key_expires_at >= ?", Time.now) if has_field?("once_key_expires_at")
75
+ scope.first
76
+ end
77
+
78
+ def find_expired_once_key(key)
79
+ return nil unless has_field?("once_key_expires_at")
80
+
81
+ record_class
82
+ .where(once_key: key, status: %w[completed error])
83
+ .where("once_key_expires_at IS NOT NULL AND once_key_expires_at < ?", Time.now)
84
+ .first
85
+ end
86
+
87
+ def find_pending_once_key(key)
88
+ record_class.where(once_key: key, status: %w[pending running]).first
89
+ end
90
+
91
+ def update_record_by_once_key(key, **attributes)
92
+ record = record_class.where(once_key: key, status: %w[completed error]).first
93
+ record&.update!(safe_attributes(attributes))
94
+ end
95
+
96
+ def unique_constraint_error?(exception)
97
+ defined?(ActiveRecord::RecordNotUnique) && exception.is_a?(ActiveRecord::RecordNotUnique)
98
+ end
99
+
52
100
  def has_field?(field_name)
53
101
  @column_set.include?(field_name.to_s)
54
102
  end
55
103
  end
56
104
 
57
105
  class MongoidAdapter < Base
106
+ def find_by_once_key(key)
107
+ now = Time.now
108
+ record_class.where(
109
+ :once_key => key,
110
+ :status.in => %w[completed error]
111
+ ).and(
112
+ record_class.or(
113
+ { once_key_expires_at: nil },
114
+ { :once_key_expires_at.gte => now }
115
+ )
116
+ ).first
117
+ end
118
+
119
+ def find_expired_once_key(key)
120
+ return nil unless has_field?("once_key_expires_at")
121
+
122
+ record_class.where(
123
+ :once_key => key,
124
+ :status.in => %w[completed error],
125
+ :once_key_expires_at.ne => nil,
126
+ :once_key_expires_at.lt => Time.now
127
+ ).first
128
+ end
129
+
130
+ def find_pending_once_key(key)
131
+ record_class.where(:once_key => key, :status.in => %w[pending running]).first
132
+ end
133
+
134
+ def update_record_by_once_key(key, **attributes)
135
+ record = record_class.where(:once_key => key, :status.in => %w[completed error]).first
136
+ record&.update!(safe_attributes(attributes))
137
+ end
138
+
139
+ def unique_constraint_error?(exception)
140
+ defined?(Mongo::Error::OperationFailure) &&
141
+ exception.is_a?(Mongo::Error::OperationFailure) &&
142
+ exception.code == 11_000
143
+ end
144
+
58
145
  def has_field?(field_name)
59
146
  record_class.fields.key?(field_name.to_s)
60
147
  end
@@ -7,26 +7,27 @@ module Dex
7
7
  def _record_wrap
8
8
  interceptor = Operation::HaltInterceptor.new { yield }
9
9
 
10
- if interceptor.success?
11
- if _record_has_pending_record?
12
- _record_update_done!(interceptor.result)
13
- elsif _record_enabled?
14
- _record_save!(interceptor.result)
15
- end
10
+ if _record_has_pending_record?
11
+ _record_update_outcome!(interceptor)
12
+ elsif _record_enabled?
13
+ _record_save!(interceptor)
16
14
  end
17
15
 
18
16
  interceptor.rethrow!
19
17
  interceptor.result
18
+ rescue => e
19
+ _record_failure!(e) if _record_has_pending_record? || _record_enabled?
20
+ raise
20
21
  end
21
22
 
22
23
  module ClassMethods
23
24
  def record(enabled = nil, **options)
24
- validate_options!(options, %i[params response], :record)
25
+ validate_options!(options, %i[params result], :record)
25
26
 
26
27
  if enabled == false
27
28
  set :record, enabled: false
28
29
  elsif enabled == true || enabled.nil?
29
- merged = { enabled: true, params: true, response: true }.merge(options)
30
+ merged = { enabled: true, params: true, result: true }.merge(options)
30
31
  set :record, **merged
31
32
  else
32
33
  raise ArgumentError,
@@ -49,27 +50,78 @@ module Dex
49
50
  defined?(@_dex_record_id) && @_dex_record_id
50
51
  end
51
52
 
52
- def _record_save!(result)
53
- Dex.record_backend.create_record(_record_attributes(result))
53
+ def _record_save!(interceptor)
54
+ attrs = _record_base_attrs
55
+ if interceptor.error?
56
+ attrs.merge!(_record_error_attrs(code: interceptor.halt.error_code,
57
+ message: interceptor.halt.error_message,
58
+ details: interceptor.halt.error_details))
59
+ else
60
+ attrs.merge!(_record_success_attrs(interceptor.result))
61
+ end
62
+ Dex.record_backend.create_record(attrs)
54
63
  rescue => e
55
64
  _record_handle_error(e)
56
65
  end
57
66
 
58
- def _record_update_done!(result)
59
- attrs = { status: "done", performed_at: _record_current_time }
60
- attrs[:response] = _record_response(result) if _record_response?
67
+ def _record_update_outcome!(interceptor)
68
+ attrs = if interceptor.error?
69
+ _record_error_attrs(code: interceptor.halt.error_code,
70
+ message: interceptor.halt.error_message,
71
+ details: interceptor.halt.error_details)
72
+ else
73
+ _record_success_attrs(interceptor.result)
74
+ end
61
75
  Dex.record_backend.update_record(@_dex_record_id, attrs)
62
76
  rescue => e
63
77
  _record_handle_error(e)
64
78
  end
65
79
 
66
- def _record_attributes(result)
67
- attrs = { name: self.class.name, performed_at: _record_current_time, status: "done" }
80
+ def _record_failure!(exception)
81
+ attrs = if exception.is_a?(Dex::Error)
82
+ _record_error_attrs(code: exception.code, message: exception.message, details: exception.details)
83
+ else
84
+ {
85
+ status: "failed",
86
+ error_code: exception.class.name,
87
+ error_message: exception.message,
88
+ performed_at: _record_current_time
89
+ }
90
+ end
91
+
92
+ attrs[:once_key] = nil if defined?(@_once_key) || self.class.settings_for(:once).fetch(:defined, false)
93
+
94
+ if _record_has_pending_record?
95
+ Dex.record_backend.update_record(@_dex_record_id, attrs)
96
+ else
97
+ Dex.record_backend.create_record(_record_base_attrs.merge(attrs))
98
+ end
99
+ rescue => e
100
+ _record_handle_error(e)
101
+ end
102
+
103
+ def _record_base_attrs
104
+ attrs = { name: self.class.name }
68
105
  attrs[:params] = _record_params? ? _record_params : nil
69
- attrs[:response] = _record_response? ? _record_response(result) : nil
70
106
  attrs
71
107
  end
72
108
 
109
+ def _record_success_attrs(result)
110
+ attrs = { status: "completed", performed_at: _record_current_time }
111
+ attrs[:result] = _record_result(result) if _record_result?
112
+ attrs
113
+ end
114
+
115
+ def _record_error_attrs(code:, message:, details:)
116
+ {
117
+ status: "error",
118
+ error_code: code.to_s,
119
+ error_message: message || code.to_s,
120
+ error_details: _record_sanitize_details(details),
121
+ performed_at: _record_current_time
122
+ }
123
+ end
124
+
73
125
  def _record_params
74
126
  _props_as_json
75
127
  end
@@ -78,11 +130,11 @@ module Dex
78
130
  self.class.settings_for(:record).fetch(:params, true)
79
131
  end
80
132
 
81
- def _record_response?
82
- self.class.settings_for(:record).fetch(:response, true)
133
+ def _record_result?
134
+ self.class.settings_for(:record).fetch(:result, true)
83
135
  end
84
136
 
85
- def _record_response(result)
137
+ def _record_result(result)
86
138
  success_type = self.class.respond_to?(:_success_type) && self.class._success_type
87
139
 
88
140
  if success_type
@@ -91,7 +143,7 @@ module Dex
91
143
  case result
92
144
  when nil then nil
93
145
  when Hash then result
94
- else { value: result }
146
+ else { _dex_value: result } # namespaced key so replay can distinguish wrapped primitives from user hashes
95
147
  end
96
148
  end
97
149
  end
@@ -100,6 +152,21 @@ module Dex
100
152
  Time.respond_to?(:current) ? Time.current : Time.now
101
153
  end
102
154
 
155
+ def _record_sanitize_details(details)
156
+ _record_sanitize_value(details)
157
+ end
158
+
159
+ def _record_sanitize_value(value)
160
+ case value
161
+ when NilClass, String, Integer, Float, TrueClass, FalseClass then value
162
+ when Symbol then value.to_s
163
+ when Hash then value.transform_values { |v| _record_sanitize_value(v) }
164
+ when Array then value.map { |v| _record_sanitize_value(v) }
165
+ when Exception then "#{value.class}: #{value.message}"
166
+ else value.to_s
167
+ end
168
+ end
169
+
103
170
  def _record_handle_error(error)
104
171
  Dex.warn("Failed to record operation: #{error.message}")
105
172
  end
data/lib/dex/operation.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  # Wrapper modules (loaded before class body so `include`/`use` can find them)
4
4
  require_relative "operation/result_wrapper"
5
+ require_relative "operation/once_wrapper"
5
6
  require_relative "operation/record_wrapper"
6
7
  require_relative "operation/transaction_wrapper"
7
8
  require_relative "operation/lock_wrapper"
@@ -9,6 +10,7 @@ require_relative "operation/async_wrapper"
9
10
  require_relative "operation/safe_wrapper"
10
11
  require_relative "operation/rescue_wrapper"
11
12
  require_relative "operation/callback_wrapper"
13
+ require_relative "operation/guard_wrapper"
12
14
 
13
15
  module Dex
14
16
  class Operation
@@ -40,23 +42,65 @@ module Dex
40
42
  end
41
43
  end
42
44
 
43
- RESERVED_PROP_NAMES = %i[call perform async safe initialize].to_set.freeze
45
+ RESERVED_PROP_NAMES = %i[call perform async safe once initialize].to_set.freeze
44
46
 
45
47
  include Executable
46
48
  include PropsSetup
47
49
  include TypeCoercion
50
+ include ContextSetup
48
51
 
49
- Contract = Data.define(:params, :success, :errors)
52
+ Contract = Data.define(:params, :success, :errors, :guards) do
53
+ attr_reader :source_class
54
+
55
+ def initialize(params:, success:, errors:, guards:, source_class: nil)
56
+ @source_class = source_class
57
+ super(params: params, success: success, errors: errors, guards: guards)
58
+ end
59
+
60
+ def to_h
61
+ if @source_class
62
+ Operation::Export.build_hash(@source_class, self)
63
+ else
64
+ super
65
+ end
66
+ end
67
+
68
+ def to_json_schema(**options)
69
+ unless @source_class
70
+ raise ArgumentError, "to_json_schema requires a source_class (use OperationClass.contract.to_json_schema)"
71
+ end
72
+
73
+ Operation::Export.build_json_schema(@source_class, self, **options)
74
+ end
75
+ end
76
+
77
+ extend Registry
50
78
 
51
79
  class << self
52
80
  def contract
53
81
  Contract.new(
54
82
  params: _contract_params,
55
83
  success: _success_type,
56
- errors: _declared_errors
84
+ errors: _declared_errors,
85
+ guards: _contract_guards,
86
+ source_class: self
57
87
  )
58
88
  end
59
89
 
90
+ def export(format: :hash, **options)
91
+ unless %i[hash json_schema].include?(format)
92
+ raise ArgumentError, "unknown format: #{format.inspect}. Known: :hash, :json_schema"
93
+ end
94
+
95
+ sorted = registry.sort_by(&:name)
96
+ sorted.map do |klass|
97
+ case format
98
+ when :hash then klass.contract.to_h
99
+ when :json_schema then klass.contract.to_json_schema(**options)
100
+ end
101
+ end
102
+ end
103
+
60
104
  private
61
105
 
62
106
  def _contract_params
@@ -66,6 +110,14 @@ module Dex
66
110
  hash[prop.name] = prop.type
67
111
  end
68
112
  end
113
+
114
+ def _contract_guards
115
+ return [] unless respond_to?(:_guard_list)
116
+
117
+ _guard_list.map do |g|
118
+ { name: g.name, message: g.message, requires: g.requires }
119
+ end
120
+ end
69
121
  end
70
122
 
71
123
  def perform(*, **)
@@ -88,9 +140,11 @@ module Dex
88
140
  include SafeWrapper
89
141
 
90
142
  use ResultWrapper
143
+ use GuardWrapper
144
+ use OnceWrapper
91
145
  use LockWrapper
92
- use TransactionWrapper
93
146
  use RecordWrapper
147
+ use TransactionWrapper
94
148
  use RescueWrapper
95
149
  use CallbackWrapper
96
150
  end
@@ -102,6 +156,10 @@ require_relative "operation/async_proxy"
102
156
  require_relative "operation/record_backend"
103
157
  require_relative "operation/transaction_adapter"
104
158
  require_relative "operation/jobs"
159
+ require_relative "operation/explain"
160
+ require_relative "operation/export"
161
+
162
+ Dex::Operation.extend(Dex::Operation::Explain)
105
163
 
106
164
  # Top-level aliases (depend on Operation::Ok/Err)
107
165
  require_relative "match"
@@ -17,10 +17,17 @@ module Dex
17
17
  end
18
18
 
19
19
  module ClassMethods
20
- def prop(name, type, kind = :keyword, **options, &block)
20
+ def prop(name, type, kind = :keyword, desc: nil, **options, &block)
21
21
  if const_defined?(:RESERVED_PROP_NAMES) && self::RESERVED_PROP_NAMES.include?(name)
22
22
  raise ArgumentError, "Property :#{name} is reserved."
23
23
  end
24
+ if !desc.nil?
25
+ raise ArgumentError, "desc: must be a String, got #{desc.class}" unless desc.is_a?(String)
26
+
27
+ _prop_desc_own[name] = desc
28
+ elsif superclass.respond_to?(:prop_descriptions) && superclass.prop_descriptions.key?(name)
29
+ _prop_desc_own[name] = nil
30
+ end
24
31
  options[:reader] = :public unless options.key?(:reader)
25
32
  if type.is_a?(Dex::RefType) && !block
26
33
  ref = type
@@ -29,7 +36,12 @@ module Dex
29
36
  super(name, type, kind, **options, &block)
30
37
  end
31
38
 
32
- def prop?(name, type, kind = :keyword, **options, &block)
39
+ def prop?(name, type, kind = :keyword, desc: nil, **options, &block)
40
+ if !desc.nil?
41
+ raise ArgumentError, "desc: must be a String, got #{desc.class}" unless desc.is_a?(String)
42
+
43
+ _prop_desc_own[name] = desc
44
+ end
33
45
  options[:reader] = :public unless options.key?(:reader)
34
46
  options[:default] = nil unless options.key?(:default)
35
47
  if type.is_a?(Dex::RefType) && !block
@@ -39,9 +51,20 @@ module Dex
39
51
  prop(name, _Nilable(type), kind, **options, &block)
40
52
  end
41
53
 
54
+ def prop_descriptions
55
+ parent = superclass.respond_to?(:prop_descriptions) ? superclass.prop_descriptions : {}
56
+ parent.merge(_prop_desc_own).compact
57
+ end
58
+
42
59
  def _Ref(model_class, lock: false) # rubocop:disable Naming/MethodName
43
60
  Dex::RefType.new(model_class, lock: lock)
44
61
  end
62
+
63
+ private
64
+
65
+ def _prop_desc_own
66
+ @_prop_desc_own ||= {}
67
+ end
45
68
  end
46
69
  end
47
70
  end