lazy_graph 0.1.6 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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