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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +36 -0
- data/README.md +50 -18
- data/gemfiles/mongoid_no_ar.gemfile +10 -0
- data/gemfiles/mongoid_no_ar.gemfile.lock +232 -0
- data/guides/llm/EVENT.md +41 -23
- data/guides/llm/FORM.md +202 -61
- data/guides/llm/OPERATION.md +49 -20
- data/guides/llm/QUERY.md +52 -2
- data/lib/dex/context_dsl.rb +56 -0
- data/lib/dex/context_setup.rb +2 -33
- data/lib/dex/event/bus.rb +85 -8
- data/lib/dex/event/handler.rb +18 -0
- data/lib/dex/event/metadata.rb +16 -9
- data/lib/dex/event/processor.rb +1 -1
- data/lib/dex/event/test_helpers.rb +88 -0
- data/lib/dex/event/trace.rb +14 -27
- data/lib/dex/event.rb +2 -7
- data/lib/dex/event_test_helpers.rb +1 -86
- data/lib/dex/form/context.rb +27 -0
- data/lib/dex/form/export.rb +128 -0
- data/lib/dex/form/nesting.rb +2 -0
- data/lib/dex/form/uniqueness_validator.rb +17 -1
- data/lib/dex/form.rb +119 -3
- data/lib/dex/id.rb +38 -0
- data/lib/dex/operation/async_proxy.rb +13 -2
- data/lib/dex/operation/explain.rb +11 -7
- data/lib/dex/operation/jobs.rb +5 -4
- data/lib/dex/operation/lock_wrapper.rb +15 -2
- data/lib/dex/operation/once_wrapper.rb +24 -15
- data/lib/dex/operation/record_backend.rb +15 -1
- data/lib/dex/operation/record_wrapper.rb +43 -8
- data/lib/dex/operation/test_helpers/assertions.rb +359 -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 +160 -0
- data/lib/dex/operation/trace_wrapper.rb +20 -0
- data/lib/dex/operation/transaction_adapter.rb +29 -68
- data/lib/dex/operation/transaction_wrapper.rb +10 -16
- data/lib/dex/operation.rb +2 -0
- data/lib/dex/query/backend.rb +13 -0
- data/lib/dex/query/export.rb +64 -0
- data/lib/dex/query.rb +50 -5
- data/lib/dex/ref_type.rb +4 -0
- data/lib/dex/test_helpers.rb +4 -139
- data/lib/dex/test_log.rb +62 -4
- data/lib/dex/trace.rb +291 -0
- data/lib/dex/type_coercion.rb +4 -1
- data/lib/dex/version.rb +1 -1
- data/lib/dexkit.rb +9 -5
- metadata +16 -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,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
|
|
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/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
|
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
|
|
@@ -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
|
|
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 ==
|
|
265
|
-
raise ArgumentError, "Scope model mismatch: expected #{base.klass}, got #{
|
|
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(
|
|
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
|