dexkit 0.9.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.
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
data/lib/dex/test_log.rb CHANGED
@@ -2,7 +2,18 @@
2
2
 
3
3
  module Dex
4
4
  module TestLog
5
- Entry = Data.define(:type, :name, :operation_class, :params, :result, :duration, :caller_location)
5
+ Entry = Data.define(
6
+ :type,
7
+ :name,
8
+ :operation_class,
9
+ :params,
10
+ :result,
11
+ :duration,
12
+ :caller_location,
13
+ :execution_id,
14
+ :trace_id,
15
+ :trace
16
+ )
6
17
 
7
18
  @_entries = []
8
19
  @_mutex = Mutex.new
@@ -42,14 +53,61 @@ module Dex
42
53
  return "No operations called." if entries.empty?
43
54
 
44
55
  lines = ["Operations called (#{entries.size}):"]
45
- entries.each_with_index do |entry, i|
56
+ nodes = entries.each_with_index.map { |entry, index| { entry: entry, index: index } }
57
+ by_id = nodes.to_h { |node| [node[:entry].execution_id, node] }
58
+ children = Hash.new { |hash, key| hash[key] = [] }
59
+ roots = []
60
+
61
+ nodes.each do |node|
62
+ parent_id = _parent_operation_id(node[:entry])
63
+ if parent_id && by_id[parent_id]
64
+ children[parent_id] << node
65
+ else
66
+ roots << node
67
+ end
68
+ end
69
+
70
+ counter = 0
71
+ render = lambda do |node, depth|
72
+ counter += 1
73
+ entry = node[:entry]
46
74
  status = entry.result.ok? ? "OK" : "ERR(#{entry.result.code})"
47
75
  duration_ms = entry.duration ? format("%.1fms", entry.duration * 1000) : "n/a"
48
- lines << " #{i + 1}. #{entry.name} [#{status}] #{duration_ms}"
49
- lines << " params: #{entry.params.inspect}" unless entry.params.nil? || entry.params.empty?
76
+ indent = " " * depth
77
+ id = entry.execution_id ? " (#{_display_id(entry.execution_id)})" : ""
78
+
79
+ lines << " #{indent}#{counter}. #{entry.name}#{id} [#{status}] #{duration_ms}"
80
+ lines << " #{indent} params: #{entry.params.inspect}" unless entry.params.nil? || entry.params.empty?
81
+
82
+ children.fetch(entry.execution_id, []).sort_by { |child| child[:index] }.each do |child|
83
+ render.call(child, depth + 1)
84
+ end
50
85
  end
86
+
87
+ roots.sort_by { |node| node[:index] }.each { |node| render.call(node, 0) }
51
88
  lines.join("\n")
52
89
  end
90
+
91
+ private
92
+
93
+ def _frame_type(frame)
94
+ return unless frame
95
+
96
+ (frame[:type] || frame["type"])&.to_sym
97
+ end
98
+
99
+ def _display_id(id)
100
+ prefix, suffix = id.to_s.split("_", 2)
101
+ return id.to_s unless suffix
102
+
103
+ "#{prefix}_#{suffix[0, 7]}"
104
+ end
105
+
106
+ def _parent_operation_id(entry)
107
+ frames = Array(entry.trace)[0...-1]
108
+ parent = frames.reverse.find { |frame| _frame_type(frame) == :operation }
109
+ parent && (parent[:id] || parent["id"])
110
+ end
53
111
  end
54
112
  end
55
113
  end
data/lib/dex/trace.rb ADDED
@@ -0,0 +1,291 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dex
4
+ module Trace
5
+ FIBER_KEY = :_dex_trace
6
+ FRAME_TYPES = %i[actor operation handler].freeze
7
+
8
+ class << self
9
+ def start(actor: nil, trace_id: nil)
10
+ previous = _dump_state
11
+ _set_state(trace_id: (trace_id || Dex::Id.generate("tr_")).to_s, frames: [], event_context: nil)
12
+ _state[:frames] << _normalize_actor(actor) if actor
13
+ yield
14
+ ensure
15
+ _restore_state(previous)
16
+ end
17
+
18
+ def ensure_started!(trace_id: nil)
19
+ return false if active?
20
+
21
+ _state[:trace_id] = (trace_id || Dex::Id.generate("tr_")).to_s
22
+ true
23
+ end
24
+
25
+ def active?
26
+ !trace_id.nil?
27
+ end
28
+
29
+ def with_frame(frame)
30
+ auto_started = ensure_started!
31
+ pushed = false
32
+ push(frame)
33
+ pushed = true
34
+ yield
35
+ ensure
36
+ pop if pushed
37
+ stop! if auto_started
38
+ end
39
+
40
+ def with_event_context(event)
41
+ auto_started = ensure_started!(trace_id: event.trace_id)
42
+ previous = _state[:event_context]
43
+ _state[:event_context] = _build_event_context(event)
44
+ yield
45
+ ensure
46
+ _state[:event_context] = previous
47
+ stop! if auto_started
48
+ end
49
+
50
+ def restore_event_context(event_id:, trace_id:, event_class: nil, event_ancestry: [])
51
+ previous = _dump_state
52
+ effective_trace_id = trace_id&.to_s || _state[:trace_id]
53
+ _set_state(
54
+ trace_id: effective_trace_id,
55
+ frames: _normalize_frames(_state[:frames]),
56
+ event_context: {
57
+ id: event_id&.to_s,
58
+ trace_id: effective_trace_id,
59
+ event_class: event_class,
60
+ event_ancestry: Array(event_ancestry).compact.map(&:to_s)
61
+ }
62
+ )
63
+ yield
64
+ ensure
65
+ _restore_state(previous)
66
+ end
67
+
68
+ def push(frame)
69
+ _state[:frames] << _normalize_frame(frame)
70
+ end
71
+
72
+ def pop
73
+ _state[:frames].pop
74
+ end
75
+
76
+ def current
77
+ _deep_copy(_state[:frames])
78
+ end
79
+
80
+ def trace_id
81
+ _state[:trace_id]
82
+ end
83
+
84
+ def current_id
85
+ _state[:frames].last&.dig(:id)
86
+ end
87
+
88
+ def actor
89
+ frame = _state[:frames].find { |entry| entry[:type] == :actor }
90
+ frame ? _deep_copy(frame) : nil
91
+ end
92
+
93
+ def current_event_id
94
+ current_event_context&.dig(:id)
95
+ end
96
+
97
+ def current_event_context
98
+ context = _state[:event_context]
99
+ return _deep_copy(context) if context
100
+
101
+ handler = _state[:frames].reverse.find { |frame| frame[:type] == :handler && frame[:event_id] }
102
+ return nil unless handler
103
+
104
+ {
105
+ id: handler[:event_id],
106
+ trace_id: trace_id,
107
+ event_class: handler[:event_class],
108
+ event_ancestry: _deep_copy(Array(handler[:event_ancestry]))
109
+ }
110
+ end
111
+
112
+ def dump
113
+ return nil unless active?
114
+
115
+ data = {
116
+ trace_id: trace_id,
117
+ frames: current
118
+ }
119
+ data[:event_context] = _deep_copy(_state[:event_context]) if _state[:event_context]
120
+ data
121
+ end
122
+
123
+ def restore(data)
124
+ return yield unless data
125
+
126
+ previous = _dump_state
127
+ _set_state(
128
+ trace_id: _fetch(data, :trace_id)&.to_s,
129
+ frames: _normalize_frames(_fetch(data, :frames)),
130
+ event_context: _normalize_event_context(_fetch(data, :event_context))
131
+ )
132
+ yield
133
+ ensure
134
+ _restore_state(previous)
135
+ end
136
+
137
+ def to_s
138
+ current.filter_map { |frame| _format_frame(frame) }.join(" > ")
139
+ end
140
+
141
+ def clear!
142
+ Fiber[FIBER_KEY] = nil
143
+ end
144
+ alias_method :stop!, :clear!
145
+
146
+ private
147
+
148
+ def _state
149
+ Fiber[FIBER_KEY] ||= { trace_id: nil, frames: [], event_context: nil }
150
+ end
151
+
152
+ def _set_state(trace_id:, frames:, event_context:)
153
+ Fiber[FIBER_KEY] = {
154
+ trace_id: trace_id,
155
+ frames: frames,
156
+ event_context: event_context
157
+ }
158
+ end
159
+
160
+ def _dump_state
161
+ state = _state
162
+ return nil unless state[:trace_id] || !state[:frames].empty? || state[:event_context]
163
+
164
+ {
165
+ trace_id: state[:trace_id],
166
+ frames: _deep_copy(state[:frames]),
167
+ event_context: _deep_copy(state[:event_context])
168
+ }
169
+ end
170
+
171
+ def _restore_state(state)
172
+ if state
173
+ _set_state(
174
+ trace_id: state[:trace_id],
175
+ frames: _normalize_frames(state[:frames]),
176
+ event_context: _normalize_event_context(state[:event_context])
177
+ )
178
+ else
179
+ clear!
180
+ end
181
+ end
182
+
183
+ def _normalize_frames(frames)
184
+ Array(frames).map { |frame| _normalize_frame(frame) }
185
+ end
186
+
187
+ def _normalize_actor(actor)
188
+ raise ArgumentError, "actor must be a Hash" unless actor.is_a?(Hash)
189
+
190
+ normalized = _symbolize(actor)
191
+ actor_type = normalized[:type]
192
+ raise ArgumentError, "actor must include :type" if actor_type.nil? || actor_type.to_s.strip.empty?
193
+
194
+ frame = { type: :actor, actor_type: actor_type.to_s }
195
+ frame[:id] = normalized[:id].to_s if normalized.key?(:id) && !normalized[:id].nil?
196
+
197
+ normalized.each do |key, value|
198
+ next if %i[type id].include?(key)
199
+
200
+ frame[key] = _deep_copy(value)
201
+ end
202
+
203
+ frame
204
+ end
205
+
206
+ def _normalize_frame(frame)
207
+ raise ArgumentError, "trace frame must be a Hash" unless frame.is_a?(Hash)
208
+
209
+ normalized = _symbolize(frame)
210
+ type = normalized[:type]&.to_sym
211
+ raise ArgumentError, "trace frame type is required" unless type
212
+ raise ArgumentError, "unknown trace frame type: #{type.inspect}" unless FRAME_TYPES.include?(type)
213
+
214
+ normalized[:type] = type
215
+ normalized[:id] = normalized[:id].to_s if normalized.key?(:id) && !normalized[:id].nil?
216
+ normalized[:actor_type] = normalized[:actor_type].to_s if normalized.key?(:actor_type) && !normalized[:actor_type].nil?
217
+ normalized[:event_id] = normalized[:event_id].to_s if normalized.key?(:event_id) && !normalized[:event_id].nil?
218
+ normalized[:event_ancestry] = Array(normalized[:event_ancestry]).compact.map(&:to_s) if normalized.key?(:event_ancestry)
219
+ normalized
220
+ end
221
+
222
+ def _normalize_event_context(context)
223
+ return nil unless context
224
+
225
+ {
226
+ id: _fetch(context, :id)&.to_s,
227
+ trace_id: _fetch(context, :trace_id)&.to_s,
228
+ event_class: _fetch(context, :event_class),
229
+ event_ancestry: Array(_fetch(context, :event_ancestry)).compact.map(&:to_s)
230
+ }
231
+ end
232
+
233
+ def _build_event_context(event)
234
+ {
235
+ id: event.id.to_s,
236
+ trace_id: event.trace_id.to_s,
237
+ event_class: event.class.name,
238
+ event_ancestry: Array(event.metadata.event_ancestry).compact.map(&:to_s)
239
+ }
240
+ end
241
+
242
+ def _symbolize(hash)
243
+ hash.each_with_object({}) do |(key, value), result|
244
+ result[key.respond_to?(:to_sym) ? key.to_sym : key] = _deep_copy(value)
245
+ end
246
+ end
247
+
248
+ def _deep_copy(value)
249
+ case value
250
+ when Hash
251
+ value.each_with_object({}) { |(key, nested), result| result[key] = _deep_copy(nested) }
252
+ when Array
253
+ value.map { |nested| _deep_copy(nested) }
254
+ else
255
+ value
256
+ end
257
+ end
258
+
259
+ def _fetch(hash, key)
260
+ hash[key] || hash[key.to_s]
261
+ end
262
+
263
+ def _format_frame(frame)
264
+ case frame[:type]
265
+ when :actor
266
+ return nil unless frame[:actor_type]
267
+
268
+ frame[:id] ? "#{frame[:actor_type]}:#{frame[:id]}" : frame[:actor_type]
269
+ when :operation
270
+ name = frame[:class] || "Operation"
271
+ id = _display_id(frame[:id])
272
+ id ? "#{name}(#{id})" : name
273
+ when :handler
274
+ event_class = frame[:event_class] || "Event"
275
+ handler_name = frame[:class] || "Handler"
276
+ id = _display_id(frame[:id])
277
+ id ? "[#{event_class}] #{handler_name}(#{id})" : "[#{event_class}] #{handler_name}"
278
+ end
279
+ end
280
+
281
+ def _display_id(id)
282
+ return nil unless id
283
+
284
+ prefix, suffix = id.split("_", 2)
285
+ return id[0, 10] unless suffix
286
+
287
+ "#{prefix}_#{suffix[0, 7]}"
288
+ end
289
+ end
290
+ end
291
+ end
data/lib/dex/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dex
4
- VERSION = "0.9.0"
4
+ VERSION = "0.10.0"
5
5
  end
data/lib/dexkit.rb CHANGED
@@ -10,6 +10,7 @@ require_relative "dex/concern"
10
10
  require_relative "dex/ref_type"
11
11
  require_relative "dex/type_coercion"
12
12
  require_relative "dex/props_setup"
13
+ require_relative "dex/context_dsl"
13
14
  require_relative "dex/context_setup"
14
15
  require_relative "dex/error"
15
16
  require_relative "dex/settings"
@@ -17,6 +18,8 @@ require_relative "dex/pipeline"
17
18
  require_relative "dex/executable"
18
19
  require_relative "dex/registry"
19
20
  require_relative "dex/type_serializer"
21
+ require_relative "dex/id"
22
+ require_relative "dex/trace"
20
23
  require_relative "dex/operation"
21
24
  require_relative "dex/event"
22
25
  require_relative "dex/tool"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dexkit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jacek Galanciak
@@ -168,6 +168,7 @@ files:
168
168
  - guides/llm/OPERATION.md
169
169
  - guides/llm/QUERY.md
170
170
  - lib/dex/concern.rb
171
+ - lib/dex/context_dsl.rb
171
172
  - lib/dex/context_setup.rb
172
173
  - lib/dex/error.rb
173
174
  - lib/dex/event.rb
@@ -184,8 +185,11 @@ files:
184
185
  - lib/dex/event_test_helpers.rb
185
186
  - lib/dex/executable.rb
186
187
  - lib/dex/form.rb
188
+ - lib/dex/form/context.rb
189
+ - lib/dex/form/export.rb
187
190
  - lib/dex/form/nesting.rb
188
191
  - lib/dex/form/uniqueness_validator.rb
192
+ - lib/dex/id.rb
189
193
  - lib/dex/match.rb
190
194
  - lib/dex/operation.rb
191
195
  - lib/dex/operation/async_proxy.rb
@@ -207,12 +211,14 @@ files:
207
211
  - lib/dex/operation/test_helpers/assertions.rb
208
212
  - lib/dex/operation/test_helpers/execution.rb
209
213
  - lib/dex/operation/test_helpers/stubbing.rb
214
+ - lib/dex/operation/trace_wrapper.rb
210
215
  - lib/dex/operation/transaction_adapter.rb
211
216
  - lib/dex/operation/transaction_wrapper.rb
212
217
  - lib/dex/pipeline.rb
213
218
  - lib/dex/props_setup.rb
214
219
  - lib/dex/query.rb
215
220
  - lib/dex/query/backend.rb
221
+ - lib/dex/query/export.rb
216
222
  - lib/dex/query/filtering.rb
217
223
  - lib/dex/query/sorting.rb
218
224
  - lib/dex/railtie.rb
@@ -222,6 +228,7 @@ files:
222
228
  - lib/dex/test_helpers.rb
223
229
  - lib/dex/test_log.rb
224
230
  - lib/dex/tool.rb
231
+ - lib/dex/trace.rb
225
232
  - lib/dex/type_coercion.rb
226
233
  - lib/dex/type_serializer.rb
227
234
  - lib/dex/version.rb