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
@@ -1,333 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Dex
4
- module TestHelpers
5
- # --- Result assertions ---
6
-
7
- def assert_ok(result, expected = :_not_given, msg = nil, &block)
8
- assert result.ok?, msg || "Expected Ok, got Err:\n#{_dex_format_err(result)}"
9
- if expected != :_not_given
10
- assert_equal expected, result.value, msg || "Ok value mismatch"
11
- end
12
- yield result.value if block
13
- result
14
- end
15
-
16
- def refute_ok(result, msg = nil)
17
- refute result.ok?, msg || "Expected Err, got Ok:\n#{_dex_format_ok(result)}"
18
- result
19
- end
20
-
21
- def assert_err(result, code = nil, message: nil, details: nil, msg: nil, &block)
22
- assert result.error?, msg || "Expected Err, got Ok:\n#{_dex_format_ok(result)}"
23
- if code
24
- assert_equal code, result.code, msg || "Error code mismatch.\n#{_dex_format_err(result)}"
25
- end
26
- if message
27
- case message
28
- when Regexp
29
- assert_match message, result.message, msg || "Error message mismatch.\n#{_dex_format_err(result)}"
30
- else
31
- assert_equal message, result.message, msg || "Error message mismatch.\n#{_dex_format_err(result)}"
32
- end
33
- end
34
- details&.each do |key, val|
35
- assert_equal val, result.details&.dig(key),
36
- msg || "Error details[:#{key}] mismatch.\n#{_dex_format_err(result)}"
37
- end
38
- yield result.error if block
39
- result
40
- end
41
-
42
- def refute_err(result, code = nil, msg: nil)
43
- if code
44
- if result.error?
45
- refute_equal code, result.code,
46
- msg || "Expected result to not have error code #{code.inspect}, but it does.\n#{_dex_format_err(result)}"
47
- end
48
- else
49
- refute result.error?, msg || "Expected Ok, got Err:\n#{_dex_format_err(result)}"
50
- end
51
- result
52
- end
53
-
54
- # --- One-liner assertions ---
55
-
56
- def assert_operation(*args, returns: :_not_given, **params)
57
- klass = _dex_resolve_subject(args)
58
- result = klass.new(**params).safe.call
59
- assert result.ok?, "Expected operation to succeed, got Err:\n#{_dex_format_err(result)}"
60
- if returns != :_not_given
61
- assert_equal returns, result.value, "Return value mismatch"
62
- end
63
- result
64
- end
65
-
66
- def assert_operation_error(*args, message: nil, details: nil, **params)
67
- klass, code = _dex_resolve_subject_and_code(args)
68
- result = klass.new(**params).safe.call
69
- assert result.error?, "Expected operation to fail, got Ok:\n#{_dex_format_ok(result)}"
70
- if code
71
- assert_equal code, result.code, "Error code mismatch.\n#{_dex_format_err(result)}"
72
- end
73
- if message
74
- case message
75
- when Regexp
76
- assert_match message, result.message
77
- else
78
- assert_equal message, result.message
79
- end
80
- end
81
- details&.each do |key, val|
82
- assert_equal val, result.details&.dig(key)
83
- end
84
- result
85
- end
86
-
87
- # --- Contract assertions ---
88
-
89
- def assert_params(*args)
90
- if args.last.is_a?(Hash)
91
- klass_args, type_hash = _dex_split_class_and_hash(args)
92
- klass = _dex_resolve_subject(klass_args)
93
- contract = klass.contract
94
- type_hash.each do |name, type|
95
- assert contract.params.key?(name),
96
- "Expected param #{name.inspect} to be declared on #{klass.name || klass}"
97
- assert_equal type, contract.params[name],
98
- "Type mismatch for param #{name.inspect}"
99
- end
100
- else
101
- klass_args, names = _dex_split_class_and_symbols(args)
102
- klass = _dex_resolve_subject(klass_args)
103
- contract = klass.contract
104
- assert_equal names.sort, contract.params.keys.sort,
105
- "Params mismatch on #{klass.name || klass}.\n Expected: #{names.sort.inspect}\n Actual: #{contract.params.keys.sort.inspect}"
106
- end
107
- end
108
-
109
- def assert_accepts_param(*args)
110
- klass_args, names = _dex_split_class_and_symbols(args)
111
- klass = _dex_resolve_subject(klass_args)
112
- contract = klass.contract
113
- names.each do |name|
114
- assert contract.params.key?(name),
115
- "Expected #{klass.name || klass} to accept param #{name.inspect}, but it doesn't.\n Declared params: #{contract.params.keys.inspect}"
116
- end
117
- end
118
-
119
- def assert_success_type(*args)
120
- klass = if args.first.is_a?(Class) && args.first < Dex::Operation
121
- args.shift
122
- else
123
- _dex_resolve_subject([])
124
- end
125
- expected = args.first
126
- contract = klass.contract
127
- assert_equal expected, contract.success,
128
- "Success type mismatch on #{klass.name || klass}"
129
- end
130
-
131
- def assert_error_codes(*args)
132
- klass_args, codes = _dex_split_class_and_symbols(args)
133
- klass = _dex_resolve_subject(klass_args)
134
- contract = klass.contract
135
- assert_equal codes.sort, contract.errors.sort,
136
- "Error codes mismatch on #{klass.name || klass}.\n Expected: #{codes.sort.inspect}\n Actual: #{contract.errors.sort.inspect}"
137
- end
138
-
139
- def assert_contract(*args, params: nil, success: :_not_given, errors: nil)
140
- klass = _dex_resolve_subject(args)
141
- contract = klass.contract
142
-
143
- if params
144
- case params
145
- when Array
146
- assert_equal params.sort, contract.params.keys.sort, "Contract params mismatch"
147
- when Hash
148
- params.each do |name, type|
149
- assert contract.params.key?(name), "Expected param #{name.inspect}"
150
- assert_equal type, contract.params[name], "Type mismatch for param #{name.inspect}"
151
- end
152
- end
153
- end
154
-
155
- if success != :_not_given
156
- assert_equal success, contract.success, "Contract success type mismatch"
157
- end
158
-
159
- if errors
160
- assert_equal errors.sort, contract.errors.sort, "Contract error codes mismatch"
161
- end
162
- end
163
-
164
- # --- Param validation assertions ---
165
-
166
- def assert_invalid_params(*args, **params)
167
- klass = _dex_resolve_subject(args)
168
- assert_raises(Literal::TypeError) { klass.new(**params) }
169
- end
170
-
171
- def assert_valid_params(*args, **params)
172
- klass = _dex_resolve_subject(args)
173
- klass.new(**params)
174
- end
175
-
176
- # --- Async assertions ---
177
-
178
- def assert_enqueues_operation(*args, queue: nil, **params)
179
- _dex_ensure_active_job_test_helper!
180
- klass = _dex_resolve_subject(args)
181
- async_opts = queue ? { queue: queue } : {}
182
- before_count = enqueued_jobs.size
183
- klass.new(**params).async(**async_opts).call
184
- new_jobs = enqueued_jobs[before_count..]
185
- dex_job = new_jobs.find { |j|
186
- j[:job] == Dex::Operation::DirectJob || j[:job] == Dex::Operation::RecordJob
187
- }
188
- assert dex_job,
189
- "Expected #{klass.name || klass} to enqueue an async job, but none were enqueued"
190
- end
191
-
192
- def refute_enqueues_operation(&block)
193
- _dex_ensure_active_job_test_helper!
194
- before_count = enqueued_jobs.size
195
- yield
196
- after_count = enqueued_jobs.size
197
- assert_equal before_count, after_count,
198
- "Expected no operations to be enqueued, but #{after_count - before_count} were"
199
- end
200
-
201
- # --- Transaction assertions ---
202
-
203
- def assert_rolls_back(model_class, &block)
204
- count_before = model_class.count
205
- assert_raises(Dex::Error) { yield }
206
- assert_equal count_before, model_class.count,
207
- "Expected transaction to roll back, but #{model_class.name} count changed from #{count_before} to #{model_class.count}"
208
- end
209
-
210
- def assert_commits(model_class, &block)
211
- count_before = model_class.count
212
- yield
213
- assert count_before < model_class.count,
214
- "Expected #{model_class.name} count to increase, but it stayed at #{count_before}"
215
- end
216
-
217
- # --- Guard assertions ---
218
-
219
- def assert_callable(*args, **params)
220
- klass = _dex_resolve_subject(args)
221
- result = klass.callable(**params)
222
- assert result.ok?, "Expected operation to be callable, but guards failed:\n#{_dex_format_err(result)}"
223
- result
224
- end
225
-
226
- def refute_callable(*args, **params)
227
- klass_args, codes = _dex_split_class_and_symbols(args)
228
- klass = _dex_resolve_subject(klass_args)
229
- code = codes.first
230
- result = klass.callable(**params)
231
- refute result.ok?, "Expected operation to NOT be callable, but all guards passed"
232
- if code
233
- failed_codes = result.details.map { |f| f[:guard] }
234
- assert_includes failed_codes, code,
235
- "Expected guard :#{code} to fail, but it didn't.\n Failed guards: #{failed_codes.inspect}"
236
- end
237
- result
238
- end
239
-
240
- # --- Batch assertions ---
241
-
242
- def assert_all_succeed(*args, params_list:)
243
- klass = _dex_resolve_subject(args)
244
- results = params_list.map { |p| klass.new(**p).safe.call }
245
- failures = results.each_with_index.reject { |r, _| r.ok? }
246
- if failures.any?
247
- msgs = failures.map { |r, i| " [#{i}] #{params_list[i].inspect} => #{_dex_format_err(r)}" }
248
- flunk "Expected all #{results.size} calls to succeed, but #{failures.size} failed:\n#{msgs.join("\n")}"
249
- end
250
- results
251
- end
252
-
253
- def assert_all_fail(*args, code:, params_list:, message: nil, details: nil)
254
- klass = _dex_resolve_subject(args)
255
- results = params_list.map { |p| klass.new(**p).safe.call }
256
- failures = results.each_with_index.reject { |r, _| r.error? && r.code == code }
257
- if failures.any?
258
- msgs = failures.map { |r, i|
259
- status = r.ok? ? "Ok(#{r.value.inspect})" : "Err(#{r.code})"
260
- " [#{i}] #{params_list[i].inspect} => #{status}"
261
- }
262
- flunk "Expected all #{results.size} calls to fail with #{code.inspect}, but #{failures.size} didn't:\n#{msgs.join("\n")}"
263
- end
264
- results.each_with_index do |r, i|
265
- if message
266
- case message
267
- when Regexp
268
- assert_match message, r.message,
269
- "Error message mismatch at [#{i}] #{params_list[i].inspect}.\n#{_dex_format_err(r)}"
270
- else
271
- assert_equal message, r.message,
272
- "Error message mismatch at [#{i}] #{params_list[i].inspect}.\n#{_dex_format_err(r)}"
273
- end
274
- end
275
- details&.each do |key, val|
276
- assert_equal val, r.details&.dig(key),
277
- "Error details[:#{key}] mismatch at [#{i}] #{params_list[i].inspect}.\n#{_dex_format_err(r)}"
278
- end
279
- end
280
- results
281
- end
282
-
283
- private
284
-
285
- def _dex_format_err(result)
286
- return "(not an error)" unless result.respond_to?(:error?) && result.error?
287
-
288
- lines = [" code: #{result.code.inspect}"]
289
- lines << " message: #{result.message.inspect}" if result.message && result.message != result.code.to_s
290
- lines << " details: #{result.details.inspect}" if result.details
291
- lines.join("\n")
292
- end
293
-
294
- def _dex_format_ok(result)
295
- return "(not ok)" unless result.respond_to?(:ok?) && result.ok?
296
-
297
- " value: #{result.value.inspect}"
298
- end
299
-
300
- def _dex_resolve_subject_and_code(args)
301
- if args.first.is_a?(Class) && args.first < Dex::Operation
302
- klass = args.shift
303
- code = args.shift
304
- [klass, code]
305
- elsif args.first.is_a?(Symbol)
306
- [_dex_resolve_subject([]), args.shift]
307
- else
308
- [_dex_resolve_subject([]), nil]
309
- end
310
- end
311
-
312
- def _dex_split_class_and_symbols(args)
313
- if args.first.is_a?(Class) && args.first < Dex::Operation
314
- [args[0..0], args[1..]]
315
- else
316
- [[], args]
317
- end
318
- end
319
-
320
- def _dex_split_class_and_hash(args)
321
- hash = args.pop
322
- klass_args = args.select { |a| a.is_a?(Class) && a < Dex::Operation }
323
- [klass_args, hash]
324
- end
325
-
326
- def _dex_ensure_active_job_test_helper!
327
- return if respond_to?(:assert_enqueued_with)
328
-
329
- raise "assert_enqueues_operation requires ActiveJob::TestHelper. " \
330
- "Include it in your test class: `include ActiveJob::TestHelper`"
331
- end
332
- end
333
- end
@@ -1,28 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Dex
4
- module TestHelpers
5
- def call_operation(*args, **params)
6
- klass = _dex_resolve_subject(args)
7
- klass.new(**params).safe.call
8
- end
9
-
10
- def call_operation!(*args, **params)
11
- klass = _dex_resolve_subject(args)
12
- klass.new(**params).call
13
- end
14
-
15
- private
16
-
17
- def _dex_resolve_subject(args)
18
- if args.first.is_a?(Class) && args.first < Dex::Operation
19
- args.first
20
- elsif _dex_test_subject
21
- _dex_test_subject
22
- else
23
- raise ArgumentError,
24
- "No operation class specified. Pass it as the first argument or use `testing MyOperation` in your test class."
25
- end
26
- end
27
- end
28
- end
@@ -1,59 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Dex
4
- module TestHelpers
5
- def stub_operation(klass, returns: nil, error: nil, &block)
6
- raise ArgumentError, "stub_operation requires a block" unless block
7
-
8
- opts = if error
9
- { error: error }
10
- else
11
- { returns: returns }
12
- end
13
-
14
- Dex::TestWrapper.register_stub(klass, **opts)
15
- yield
16
- ensure
17
- Dex::TestWrapper.clear_stub(klass)
18
- end
19
-
20
- def spy_on_operation(klass, &block)
21
- spy = Spy.new(klass)
22
- yield spy
23
- spy
24
- end
25
-
26
- class Spy
27
- def initialize(klass)
28
- @klass = klass
29
- @started_at = Dex::TestLog.size
30
- end
31
-
32
- def calls
33
- Dex::TestLog.calls[@started_at..].select { |e| e.operation_class == @klass }
34
- end
35
-
36
- def called?
37
- calls.any?
38
- end
39
-
40
- def called_once?
41
- calls.size == 1
42
- end
43
-
44
- def call_count
45
- calls.size
46
- end
47
-
48
- def last_result
49
- calls.last&.result
50
- end
51
-
52
- def called_with?(**params)
53
- calls.any? do |entry|
54
- params.all? { |k, v| entry.params[k] == v }
55
- end
56
- end
57
- end
58
- end
59
- end