lazy_graph 0.1.6 → 0.2.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,87 @@
1
+ require 'logger'
2
+ require_relative 'environment'
3
+
4
+ module LazyGraph
5
+ class << self
6
+ attr_accessor :logger
7
+ end
8
+
9
+ module Logger
10
+ COLORIZED_LOGS = !ENV['DISABLED_COLORIZED_LOGS'] && ENV.fetch('RACK_ENV', 'development') == 'development'
11
+
12
+ module_function
13
+
14
+ class << self
15
+ attr_accessor :color_enabled, :structured
16
+ end
17
+
18
+ def structured
19
+ return @structured if defined?(@structured)
20
+ @structured = !LazyGraph::Environment.development?
21
+ end
22
+
23
+ def default_logger
24
+ logger = ::Logger.new($stdout)
25
+ self.color_enabled ||= Logger::COLORIZED_LOGS
26
+ if self.color_enabled
27
+ logger.formatter = proc do |severity, datetime, progname, message|
28
+ light_gray_timestamp = "\e[90m[##{Process.pid}] #{datetime.strftime('%Y-%m-%dT%H:%M:%S.%6N')}\e[0m" # Light gray timestamp
29
+ "#{light_gray_timestamp} \e[1m#{severity}\e[0m #{progname}: #{message}\n"
30
+ end
31
+ elsif self.structured
32
+ logger.formatter = proc do |severity, datetime, progname, message|
33
+ "#{{severity:, datetime:, progname:, **(message.is_a?(Hash) ? message : {message: }) }.to_json}\n"
34
+ end
35
+ else
36
+ logger.formatter = proc do |severity, datetime, progname, message|
37
+ "[##{Process.pid}] #{datetime.strftime('%Y-%m-%dT%H:%M:%S.%6N')} #{severity} #{progname}: #{message}\n"
38
+ end
39
+ end
40
+ logger
41
+ end
42
+
43
+ def build_color_string(&blk)
44
+ return unless block_given?
45
+
46
+ instance_eval(&blk)
47
+ end
48
+
49
+ def colorize(text, color_code)
50
+ @color_enabled ? "\e[#{color_code}m#{text}\e[0m" : text
51
+ end
52
+
53
+ def green(text)
54
+ colorize(text, 32) # Green for success
55
+ end
56
+
57
+ def red(text)
58
+ colorize(text, 31) # Red for errors
59
+ end
60
+
61
+ def yellow(text)
62
+ colorize(text, 33) # Yellow for warnings or debug
63
+ end
64
+
65
+ def blue(text)
66
+ colorize(text, 34) # Blue for info
67
+ end
68
+
69
+ def light_gray(text)
70
+ colorize(text, 90) # Light gray for faded text
71
+ end
72
+
73
+ def orange(text)
74
+ colorize(text, '38;5;214')
75
+ end
76
+
77
+ def bold(text)
78
+ colorize(text, 1) # Bold text
79
+ end
80
+
81
+ def dim(text)
82
+ colorize(text, 2) # Italic text
83
+ end
84
+ end
85
+
86
+ self.logger = Logger.default_logger
87
+ end
@@ -14,6 +14,14 @@ module LazyGraph
14
14
  def to_h = nil
15
15
  def +(other) = other
16
16
  def respond_to_missing?(_method_name, _include_private = false) = true
17
+ def to_i = 0
18
+ def to_f = 0.0
19
+
20
+ def ==(other)
21
+ return true if other.nil?
22
+
23
+ super
24
+ end
17
25
 
18
26
  def method_missing(method, *args, &block)
19
27
  return super if method == :to_ary
@@ -14,7 +14,8 @@ module LazyGraph
14
14
  should_recycle = stack_memory,
15
15
  **
16
16
  )
17
- input = stack_memory.frame
17
+ return MissingValue() unless input = stack_memory.frame
18
+
18
19
  @visited[input.object_id >> 2 ^ path.shifted_id] ||= begin
19
20
  path_next = path.next
20
21
  if (path_segment = path.segment).is_a?(PathParser::PathGroup)
@@ -61,7 +62,7 @@ module LazyGraph
61
62
  end
62
63
 
63
64
  def cast(value)
64
- value
65
+ Array(value)
65
66
  end
66
67
  end
67
68
  end
@@ -44,9 +44,10 @@ module LazyGraph
44
44
  end
45
45
 
46
46
  def interpret_derived_proc(derived)
47
- src, requireds, optionals, keywords, proc_line, = DerivedRules.extract_expr_from_source_location(derived.source_location)
48
- src = src.body&.slice || ''
49
- @src = src.lines.map(&:strip)
47
+ src, requireds, optionals, keywords, loc = DerivedRules.extract_expr_from_source_location(derived.source_location)
48
+ body = src.body&.slice || ''
49
+ @src = body.lines.map(&:strip)
50
+ offset = src.slice.lines.length - body.lines.length
50
51
  inputs, conditions = parse_args_with_conditions(requireds, optionals, keywords)
51
52
 
52
53
  {
@@ -54,11 +55,11 @@ module LazyGraph
54
55
  mtime: File.mtime(derived.source_location.first),
55
56
  conditions: conditions,
56
57
  calc: instance_eval(
57
- "->(#{inputs.keys.map { |k| "#{k}=self.#{k}" }.join(', ')}){ #{src}}",
58
+ "->(#{inputs.keys.map { |k| "#{k}=self.#{k}" }.join(', ')}){ #{body}}",
58
59
  # rubocop:disable:next-line
59
60
  derived.source_location.first,
60
61
  # rubocop:enable
61
- derived.source_location.last.succ.succ
62
+ derived.source_location.last + offset
62
63
  )
63
64
  }
64
65
  end
@@ -78,13 +79,21 @@ module LazyGraph
78
79
  [keywords, conditions.any? ? conditions : nil]
79
80
  end
80
81
 
82
+ def self.get_file_body(file_path)
83
+ @file_body_cache ||= {}
84
+ if @file_body_cache[file_path]&.last.to_i < File.mtime(file_path).to_i
85
+ @file_body_cache[file_path] = [IO.readlines(file_path), File.mtime(file_path).to_i]
86
+ end
87
+ @file_body_cache[file_path]&.first
88
+ end
89
+
81
90
  def self.extract_expr_from_source_location(source_location)
82
91
  @derived_proc_cache ||= {}
83
92
  mtime = File.mtime(source_location.first).to_i
84
-
85
93
  if @derived_proc_cache[source_location]&.last.to_i.< mtime
86
94
  @derived_proc_cache[source_location] = begin
87
- source_lines = IO.readlines(source_location.first)
95
+ source_lines = get_file_body(source_location.first)
96
+
88
97
  proc_line = source_location.last - 1
89
98
  first_line = source_lines[proc_line]
90
99
  until first_line =~ /(?:lambda|proc|->)/ || proc_line.zero?
@@ -133,15 +142,16 @@ module LazyGraph
133
142
  inputs = derived[:inputs]
134
143
  case inputs
135
144
  when Symbol, String
136
- if inputs =~ PLACEHOLDER_VAR_REGEX && !derived[:calc]
145
+ if !derived[:calc]
137
146
  @src ||= inputs
138
147
  input_hash = {}
139
148
  @input_mapper = {}
140
- derived[:calc] = inputs.gsub(PLACEHOLDER_VAR_REGEX) do |match|
149
+ calc = inputs.gsub(PLACEHOLDER_VAR_REGEX) do |match|
141
150
  sub = input_hash[match[2...-1]] ||= "a#{::SecureRandom.hex(8)}"
142
151
  @input_mapper[sub.to_sym] = match[2...-1].to_sym
143
152
  sub
144
153
  end
154
+ derived[:calc] = calc unless calc == input_hash.values.first
145
155
  input_hash.invert
146
156
  else
147
157
  { inputs.to_s.gsub(/[^(?:[A-Za-z][A-Za-z0-9_])]/, '__') => inputs.to_s.freeze }
@@ -168,9 +178,20 @@ module LazyGraph
168
178
 
169
179
  def parse_rule_string(derived)
170
180
  calc_str = derived[:calc]
171
- instance_eval(
172
- "->{ begin; #{calc_str}; rescue StandardError => e; LazyGraph.logger.error(\"Exception in \#{calc_str}. \#{e.message}\"); LazyGraph.logger.error(e.backtrace.join(\"\\n\")); raise; end }", __FILE__, __LINE__
173
- )
181
+ node_path = path
182
+
183
+ src = <<~RUBY, @rule_location&.first, @rule_location&.last.to_i - 2
184
+ ->{
185
+ begin
186
+ #{calc_str}
187
+ rescue StandardError => e;
188
+ LazyGraph.logger.error("Exception in \#{calc_str} => \#{node_path}. \#{e.message}")
189
+ raise e
190
+ end
191
+ }
192
+ RUBY
193
+
194
+ instance_eval(*src)
174
195
  rescue SyntaxError
175
196
  missing_value = MissingValue { "Syntax error in #{derived[:src]}" }
176
197
  -> { missing_value }
@@ -201,18 +222,39 @@ module LazyGraph
201
222
  (segment == name ? parent.parent : @parent).find_resolver_for(segment)
202
223
  end
203
224
 
225
+ def rule_definition_backtrace
226
+ if @rule_location && @rule_location.size >= 2
227
+ rule_file, rule_line = @rule_location
228
+ rule_entry = "#{rule_file}:#{rule_line}:in `rule`"
229
+ else
230
+ rule_entry = 'unknown_rule_location'
231
+ end
232
+
233
+ current_backtrace = caller.reverse.take_while { |line| !line.include?('/lib/lazy_graph/') }.reverse
234
+ [rule_entry] + current_backtrace
235
+ end
236
+
204
237
  def map_derived_inputs_to_paths(inputs)
205
238
  inputs.values.map.with_index do |path, idx|
206
239
  segments = path.parts.map.with_index do |segment, i|
207
240
  if segment.is_a?(PathParser::PathGroup) &&
208
- segment.options.length == 1 &&
209
- (resolver = resolver_for(segment.options.first))
210
- [i, resolver]
211
- else
212
- nil
241
+ segment.options.length == 1 && !((resolver = resolver_for(segment.options.first)) || segment.options.first.segment.part.to_s =~ /\d+/)
242
+ raise(ValidationError.new(
243
+ "Invalid dependency in #{@path}: #{segment.options.first.to_path_str} cannot be resolved."
244
+ ).tap { |e| e.set_backtrace(rule_definition_backtrace) })
213
245
  end
246
+
247
+ resolver ? [i, resolver] : nil
214
248
  end.compact
215
- [path, resolver_for(path), idx, segments.any? ? segments : nil]
249
+ resolver = resolver_for(path)
250
+
251
+ unless resolver
252
+ raise(ValidationError.new(
253
+ "Invalid dependency in #{@path}: #{path.to_path_str} cannot be resolved."
254
+ ).tap { |e| e.set_backtrace(rule_definition_backtrace) })
255
+ end
256
+
257
+ [path, resolver, idx, segments.any? ? segments : nil]
216
258
  end
217
259
  end
218
260
  end
@@ -8,14 +8,27 @@ module LazyGraph
8
8
  members.each { |k| self[k] = kws[k].then { |v| v.nil? ? MissingValue::BLANK : v } }
9
9
  end
10
10
 
11
+ members.each do |m|
12
+ define_method(m) do
13
+ self[m]
14
+ end
15
+ end
16
+
17
+ alias_method :original_get, :[]
18
+
11
19
  define_method(:key?) do |x|
12
- !self[x].equal?(MissingValue::BLANK)
20
+ !original_get(x).equal?(MissingValue::BLANK)
13
21
  end
14
22
 
15
23
  define_method(:[]=) do |key, val|
16
24
  super(key, val)
17
25
  end
18
26
 
27
+ define_method(:[]) do |key|
28
+ res = original_get(key)
29
+ res.is_a?(MissingValue) ? nil : res
30
+ end
31
+
19
32
  define_method(:members) do
20
33
  members
21
34
  end
@@ -34,7 +47,7 @@ module LazyGraph
34
47
 
35
48
  def ==(other)
36
49
  return super if other.is_a?(self.class)
37
- return to_h.eql?(other.to_h) if other.respond_to?(:to_h)
50
+ return to_h.eql?(other.to_h.keep_if { |_, v| !v.nil? }) if other.respond_to?(:to_h)
38
51
 
39
52
  super
40
53
  end
@@ -47,7 +60,7 @@ module LazyGraph
47
60
 
48
61
  def get_first_of(*props)
49
62
  key = props.find do |prop|
50
- !self[prop].is_a?(MissingValue)
63
+ !original_get(prop).is_a?(MissingValue)
51
64
  end
52
65
  key ? self[key] : MissingValue::BLANK
53
66
  end
@@ -22,7 +22,7 @@ module LazyGraph
22
22
  path_next = path.next
23
23
 
24
24
  if (path_segment = path.segment).is_a?(PathParser::PathGroup)
25
- return path_segment.options.each_with_object({}.tap(&:compare_by_identity)) do |part, object|
25
+ return path_segment.options.each_with_object(SymbolHash.new) do |part, object|
26
26
  resolve(part.merge(path_next), stack_memory, nil, preserve_keys: object)
27
27
  end
28
28
  end
@@ -31,7 +31,7 @@ module LazyGraph
31
31
  node.fetch_and_resolve(path_next, input, key, stack_memory)
32
32
  end
33
33
  if @complex_pattern_properties_a.any?
34
- input.each_key do |key|
34
+ input.keys.each do |key|
35
35
  node = !@properties[key] && @complex_pattern_properties_a.find do |(pattern, _value)|
36
36
  pattern.match?(key)
37
37
  end&.last
@@ -40,12 +40,12 @@ module LazyGraph
40
40
  node.fetch_and_resolve(path_next, input, key, stack_memory)
41
41
  end
42
42
  end
43
- input
43
+ cast(input)
44
44
  elsif (prop = @properties[segment])
45
45
  prop.fetch_and_resolve(path_next, input, segment, stack_memory, preserve_keys)
46
46
  elsif (_, prop = @pattern_properties.find { |(key, _val)| key.match?(segment) })
47
47
  prop.fetch_and_resolve(path_next, input, segment, stack_memory, preserve_keys)
48
- elsif input.key?(segment)
48
+ elsif input&.key?(segment)
49
49
  prop = @properties[segment] = lazy_init_node!(input[segment], segment)
50
50
  @properties_a = @properties.to_a
51
51
  prop.fetch_and_resolve(path_next, input, segment, stack_memory, preserve_keys)
@@ -93,7 +93,11 @@ module LazyGraph
93
93
  )
94
94
  end
95
95
  define_singleton_method(:cast, lambda { |val|
96
- val.is_a?(@property_class) ? val : @property_class.new(val.to_h)
96
+ if val.is_a?(MissingValue)
97
+ val
98
+ else
99
+ val.is_a?(@property_class) ? val : @property_class.new(val.to_h)
100
+ end
97
101
  })
98
102
  end
99
103
  end
@@ -1,9 +1,10 @@
1
1
  module LazyGraph
2
2
  class ObjectNode < Node
3
3
  class SymbolHash < ::Hash
4
- def initialize(input_hash)
4
+ def initialize(input_hash = {})
5
5
  super
6
- merge!(input_hash)
6
+ merge!(input_hash.transform_keys(&:to_sym))
7
+ compare_by_identity
7
8
  end
8
9
 
9
10
  def []=(key, value)
@@ -21,6 +22,18 @@ module LazyGraph
21
22
  else super(key.to_s.to_sym)
22
23
  end
23
24
  end
25
+
26
+ def method_missing(name, *args, &block)
27
+ if key?(name)
28
+ self[name]
29
+ else
30
+ super
31
+ end
32
+ end
33
+
34
+ def respond_to_missing?(name, include_private = false)
35
+ key?(name) || super
36
+ end
24
37
  end
25
38
  end
26
39
  end
@@ -61,14 +61,18 @@ module LazyGraph
61
61
  @depth = parent ? parent.depth + 1 : 0
62
62
  @root = parent ? parent.root : self
63
63
  @rule = node[:rule]
64
+ @rule_location = node[:rule_location]
64
65
  @type = node[:type]
66
+ @validate_presence = node[:validate_presence]
65
67
  @helpers = helpers
66
- @invisible = debug ? false : node[:invisible]
68
+ @invisible = debug.eql?(true) ? false : node[:invisible]
67
69
  @visited = {}.compare_by_identity
68
70
  @namespace = namespace
69
71
 
70
72
  instance_variable_set("@is_#{@type}", true)
71
73
  define_singleton_method(:cast, build_caster)
74
+ define_singleton_method(:trace!, proc { |*| }) unless @debug
75
+
72
76
  define_missing_value_proc!
73
77
 
74
78
  @has_default = node.key?(:default)
@@ -96,7 +100,7 @@ module LazyGraph
96
100
  )
97
101
  end
98
102
 
99
- def fetch_and_resolve(path, input, segment, stack_memory, preserve_keys = nil, recurse: false)
103
+ def fetch_and_resolve(path, input, segment, stack_memory, preserve_keys = nil)
100
104
  item = fetch_item(input, segment, stack_memory)
101
105
  unless @simple || item.is_a?(MissingValue)
102
106
  item = resolve(
@@ -104,6 +108,9 @@ module LazyGraph
104
108
  stack_memory.push(item, segment)
105
109
  )
106
110
  end
111
+
112
+ item = cast(item) if @simple
113
+
107
114
  preserve_keys ? preserve_keys[segment] = item : item
108
115
  end
109
116
 
@@ -111,7 +118,13 @@ module LazyGraph
111
118
  if @is_decimal
112
119
  ->(value) { value.is_a?(BigDecimal) ? value : value.to_d }
113
120
  elsif @is_date
114
- ->(value) { value.is_a?(String) ? Date.parse(value) : value }
121
+ lambda { |value|
122
+ if value.is_a?(String)
123
+ Date.parse(value)
124
+ else
125
+ value.is_a?(Symbol) ? Date.parse(value.to_s) : value
126
+ end
127
+ }
115
128
  elsif @is_boolean
116
129
  lambda do |value|
117
130
  if value.is_a?(TrueClass) || value.is_a?(FalseClass)
@@ -131,6 +144,8 @@ module LazyGraph
131
144
  value
132
145
  end
133
146
  end
147
+ elsif @is_string
148
+ lambda(&:to_s)
134
149
  else
135
150
  ->(value) { value }
136
151
  end
@@ -206,7 +221,8 @@ module LazyGraph
206
221
  def resolve_relative_input(stack_memory, path)
207
222
  input_frame_pointer = path.absolute? ? stack_memory.root : stack_memory.ptr_at(depth - 1)
208
223
  input_frame_pointer.recursion_depth += 1
209
- return input_frame_pointer.frame[path.first_path_segment.part] if @simple
224
+
225
+ return cast(input_frame_pointer.frame[path.first_path_segment.part]) if @simple
210
226
 
211
227
  fetch_and_resolve(
212
228
  path.absolute? ? path.next.next : path.next, input_frame_pointer.frame, path.first_path_segment.part, input_frame_pointer
@@ -235,11 +251,8 @@ module LazyGraph
235
251
  if stack.recursion_depth >= 8
236
252
  input_id = key.object_id >> 2 ^ input.object_id << 28
237
253
  if @resolution_stack.key?(input_id)
238
- if @debug
239
- stack.log_debug(
240
- output: :"#{stack}.#{key}",
241
- exception: 'Infinite Recursion Detected during dependency resolution'
242
- )
254
+ trace!(stack, exception: 'Infinite Recursion Detected during dependency resolution') do
255
+ { output: :"#{stack}.#{key}" }
243
256
  end
244
257
  return MissingValue { "Infinite Recursion in #{stack} => #{key}" }
245
258
  end
@@ -260,7 +273,10 @@ module LazyGraph
260
273
  break missing_value = MissingValue { key } unless resolver
261
274
 
262
275
  part = resolver.resolve_relative_input(stack, parts[index].options.first)
263
- break missing_value = part if part.is_a?(MissingValue)
276
+ if part.is_a?(MissingValue)
277
+ raise_presence_validation_error!(stack, key, parts[index].options.first) if @validate_presence
278
+ break missing_value = part
279
+ end
264
280
 
265
281
  part_sym = part.to_s.to_sym
266
282
  parts_identity ^= part_sym.object_id << index
@@ -271,15 +287,12 @@ module LazyGraph
271
287
 
272
288
  result = missing_value || cast(resolver.resolve_relative_input(stack, path))
273
289
 
274
- if @debug
275
- stack.log_debug(
276
- output: :"#{stack}.#{key}",
277
- result: HashUtils.deep_dup(result),
278
- inputs: @node_context.to_h.except(:itself, :stack_ptr),
279
- calc: @src
280
- )
290
+ if result.nil? || result.is_a?(MissingValue)
291
+ raise_presence_validation_error!(stack, key, path) if @validate_presence
292
+ input[key] = MissingValue { key }
293
+ else
294
+ input[key] = result
281
295
  end
282
- input[key] = result.nil? ? MissingValue { key } : result
283
296
  end
284
297
 
285
298
  def derive_item!(input, key, stack)
@@ -292,7 +305,10 @@ module LazyGraph
292
305
  break missing_value = MissingValue { key } unless resolver
293
306
 
294
307
  part = resolver.resolve_relative_input(stack, parts[index].options.first)
295
- break missing_value = part if part.is_a?(MissingValue)
308
+ if part.is_a?(MissingValue)
309
+ raise_presence_validation_error!(stack, key, parts[index].options.first) if @validate_presence
310
+ break missing_value = part
311
+ end
296
312
 
297
313
  part_sym = part.to_s.to_sym
298
314
  parts_identity ^= part_sym.object_id << (index * 8)
@@ -300,8 +316,28 @@ module LazyGraph
300
316
  end
301
317
  path = @path_cache[parts_identity] ||= PathParser::Path.new(parts: parts) unless missing_value
302
318
  end
303
- result = missing_value || resolver.resolve_relative_input(stack, path)
304
- @node_context[i] = result.is_a?(MissingValue) ? nil : result
319
+ result = begin
320
+ missing_value || resolver.resolve_relative_input(stack, path)
321
+ rescue AbortError, ValidationError => e
322
+ raise e
323
+ rescue StandardError => e
324
+ ex = e
325
+ LazyGraph.logger.error("Error in #{self.path}")
326
+ LazyGraph.logger.error(e)
327
+ LazyGraph.logger.error(e.backtrace.take_while do |line|
328
+ !line.include?('lazy_graph/node.rb')
329
+ end.join("\n"))
330
+
331
+ MissingValue { "#{key} raised exception: #{e.message}" }
332
+ end
333
+
334
+ if result.nil? || result.is_a?(MissingValue)
335
+ raise_presence_validation_error!(stack, key, path) if @validate_presence
336
+
337
+ @node_context[i] = nil
338
+ else
339
+ @node_context[i] = result
340
+ end
305
341
  end
306
342
 
307
343
  @node_context[:itself] = input
@@ -316,12 +352,21 @@ module LazyGraph
316
352
  if conditions_passed
317
353
  output = begin
318
354
  cast(@fixed_result || @node_context.process!)
319
- rescue LazyGraph::AbortError => e
355
+ rescue AbortError, ValidationError => e
320
356
  raise e
321
357
  rescue StandardError => e
322
358
  ex = e
323
359
  LazyGraph.logger.error(e)
324
- LazyGraph.logger.error(e.backtrace.join("\n"))
360
+ LazyGraph.logger.error(e.backtrace.take_while do |line|
361
+ !line.include?('lazy_graph/node.rb')
362
+ end.join("\n"))
363
+
364
+ if ENV['LAZYGRAPH_OPEN_ON_ERROR'] && !@revealed_src
365
+ require 'shellwords'
366
+ @revealed_src = true
367
+ `sh -c \"$EDITOR '#{Shellwords.escape(e.backtrace.first[/.*:/][...-1])}'\" `
368
+ end
369
+
325
370
  MissingValue { "#{key} raised exception: #{e.message}" }
326
371
  end
327
372
 
@@ -330,24 +375,37 @@ module LazyGraph
330
375
  MissingValue { key }
331
376
  end
332
377
 
333
- if @debug
334
- stack.log_debug(
335
- output: :"#{stack}.#{key}",
336
- result: HashUtils.deep_dup(result),
337
- inputs: @node_context.to_h.except(:itself, :stack_ptr).transform_keys { |k| @input_mapper&.[](k) || k },
338
- calc: @src,
339
- **(@conditions ? { conditions: @conditions } : {}),
340
- **(if ex
341
- { exception: ex, backtrace: ex.backtrace.take_while do |line|
342
- !line.include?('lazy_graph/node.rb')
343
- end }
344
- else
345
- {}
346
- end
347
- )
348
- )
378
+ if conditions_passed
379
+ trace!(stack, exception: ex) do
380
+ {
381
+ output: :"#{stack}.#{key}",
382
+ result: HashUtils.deep_dup(result),
383
+ inputs: @node_context.to_h.except(:itself, :stack_ptr).transform_keys { |k| @input_mapper&.[](k) || k },
384
+ calc: @src,
385
+ **(@conditions ? { conditions: @conditions } : {})
386
+ }
387
+ end
349
388
  end
389
+
350
390
  result
351
391
  end
392
+
393
+ def trace!(stack, exception: nil)
394
+ return if @debug == 'exceptions' && !exception
395
+
396
+ trace_opts = {
397
+ **yield,
398
+ **(exception ? { exception: exception } : {})
399
+ }
400
+
401
+ return if @debug.is_a?(Regexp) && !(@debug =~ trace_opts[:output])
402
+
403
+ stack.log_debug(**trace_opts)
404
+ end
405
+
406
+ def raise_presence_validation_error!(stack, key, path)
407
+ raise ValidationError,
408
+ "Missing required value for #{stack}.#{key} at #{path.to_path_str}"
409
+ end
352
410
  end
353
411
  end