lazy_graph 0.1.3 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +282 -47
- data/Rakefile +12 -1
- data/examples/converter.rb +41 -0
- data/examples/performance_tests.rb +9 -9
- data/examples/shopping_cart_totals.rb +56 -0
- data/lib/lazy_graph/builder/dsl.rb +67 -30
- data/lib/lazy_graph/builder.rb +52 -25
- data/lib/lazy_graph/builder_group.rb +31 -18
- data/lib/lazy_graph/cli.rb +47 -0
- data/lib/lazy_graph/context.rb +37 -12
- data/lib/lazy_graph/environment.rb +6 -0
- data/lib/lazy_graph/graph.rb +28 -12
- data/lib/lazy_graph/hash_utils.rb +10 -5
- data/lib/lazy_graph/logger.rb +87 -0
- data/lib/lazy_graph/missing_value.rb +9 -2
- data/lib/lazy_graph/node/array_node.rb +14 -18
- data/lib/lazy_graph/node/derived_rules.rb +72 -17
- data/lib/lazy_graph/node/node_properties.rb +34 -9
- data/lib/lazy_graph/node/object_node.rb +41 -56
- data/lib/lazy_graph/node/symbol_hash.rb +15 -2
- data/lib/lazy_graph/node.rb +183 -94
- data/lib/lazy_graph/path_parser/path.rb +15 -7
- data/lib/lazy_graph/path_parser/path_group.rb +6 -0
- data/lib/lazy_graph/path_parser/path_part.rb +8 -0
- data/lib/lazy_graph/path_parser.rb +1 -1
- data/lib/lazy_graph/rack_app.rb +138 -0
- data/lib/lazy_graph/rack_server.rb +199 -0
- data/lib/lazy_graph/stack_pointer.rb +22 -14
- data/lib/lazy_graph/version.rb +1 -1
- data/lib/lazy_graph.rb +6 -8
- metadata +115 -11
- data/lib/lazy_graph/server.rb +0 -91
@@ -2,6 +2,8 @@ module LazyGraph
|
|
2
2
|
class ObjectNode < Node
|
3
3
|
require_relative 'symbol_hash'
|
4
4
|
|
5
|
+
attr_reader :properties
|
6
|
+
|
5
7
|
# An object supports the following types of path resolutions.
|
6
8
|
# 1. Property name: obj.property => value
|
7
9
|
# 2. Property name group: obj[property1, property2] => { property1: value1, property2: value2 }
|
@@ -14,58 +16,39 @@ module LazyGraph
|
|
14
16
|
)
|
15
17
|
input = stack_memory.frame
|
16
18
|
|
17
|
-
@visited[input.object_id >> 2 ^ path.shifted_id] ||= begin
|
19
|
+
@visited[(input.object_id >> 2 ^ path.shifted_id) + preserve_keys.object_id] ||= begin
|
18
20
|
return input if input.is_a?(MissingValue)
|
19
21
|
|
22
|
+
path_next = path.next
|
23
|
+
|
20
24
|
if (path_segment = path.segment).is_a?(PathParser::PathGroup)
|
21
|
-
return path_segment.options.each_with_object(
|
22
|
-
resolve(part.merge(
|
25
|
+
return path_segment.options.each_with_object(SymbolHash.new) do |part, object|
|
26
|
+
resolve(part.merge(path_next), stack_memory, nil, preserve_keys: object)
|
23
27
|
end
|
24
28
|
end
|
25
|
-
|
26
29
|
if !segment = path_segment&.part
|
27
|
-
@
|
28
|
-
|
29
|
-
node.resolve(path.next, stack_memory.push(item, key))
|
30
|
+
@complex_properties_a.each do |key, node|
|
31
|
+
node.fetch_and_resolve(path_next, input, key, stack_memory)
|
30
32
|
end
|
31
|
-
if @
|
32
|
-
input.
|
33
|
-
node = !@properties[key] && @
|
34
|
-
|
35
|
-
|
33
|
+
if @complex_pattern_properties_a.any?
|
34
|
+
input.keys.each do |key|
|
35
|
+
node = !@properties[key] && @complex_pattern_properties_a.find do |(pattern, _value)|
|
36
|
+
pattern.match?(key)
|
37
|
+
end&.last
|
38
|
+
next unless node
|
39
|
+
|
40
|
+
node.fetch_and_resolve(path_next, input, key, stack_memory)
|
36
41
|
end
|
37
42
|
end
|
38
|
-
input
|
43
|
+
cast(input)
|
39
44
|
elsif (prop = @properties[segment])
|
40
|
-
|
41
|
-
value = prop.resolve(
|
42
|
-
path.next, stack_memory.push(item, segment)
|
43
|
-
)
|
44
|
-
preserve_keys ? preserve_keys[segment] = value : value
|
45
|
-
elsif segment == :*
|
46
|
-
# rubocop:disable
|
47
|
-
(input.keys | @properties_a.map(&:first)).each do |key|
|
48
|
-
next unless (node = @properties[key] || @pattern_properties.find do |(pattern, _value)|
|
49
|
-
pattern.match?(key)
|
50
|
-
end&.last)
|
51
|
-
|
52
|
-
item = node.fetch_item(input, key, stack_memory)
|
53
|
-
preserve_keys[key] = node.resolve(path.next, stack_memory.push(item, key))
|
54
|
-
end
|
45
|
+
prop.fetch_and_resolve(path_next, input, segment, stack_memory, preserve_keys)
|
55
46
|
elsif (_, prop = @pattern_properties.find { |(key, _val)| key.match?(segment) })
|
56
|
-
|
57
|
-
|
58
|
-
path.next, stack_memory.push(item, segment)
|
59
|
-
)
|
60
|
-
preserve_keys ? preserve_keys[segment] = value : value
|
61
|
-
elsif input.key?(segment)
|
47
|
+
prop.fetch_and_resolve(path_next, input, segment, stack_memory, preserve_keys)
|
48
|
+
elsif input&.key?(segment)
|
62
49
|
prop = @properties[segment] = lazy_init_node!(input[segment], segment)
|
63
50
|
@properties_a = @properties.to_a
|
64
|
-
|
65
|
-
value = prop.resolve(
|
66
|
-
path.next, stack_memory.push(item, segment)
|
67
|
-
)
|
68
|
-
preserve_keys ? preserve_keys[segment] = value : value
|
51
|
+
prop.fetch_and_resolve(path_next, input, segment, stack_memory, preserve_keys)
|
69
52
|
else
|
70
53
|
value = MissingValue()
|
71
54
|
preserve_keys ? preserve_keys[segment] = value : value
|
@@ -76,10 +59,10 @@ module LazyGraph
|
|
76
59
|
end
|
77
60
|
|
78
61
|
def find_resolver_for(segment)
|
79
|
-
if segment
|
62
|
+
if segment.equal?(:'$')
|
80
63
|
root
|
81
64
|
elsif @properties.key?(segment)
|
82
|
-
|
65
|
+
@properties[segment]
|
83
66
|
else
|
84
67
|
@parent&.find_resolver_for(segment)
|
85
68
|
end
|
@@ -88,32 +71,34 @@ module LazyGraph
|
|
88
71
|
def children=(value)
|
89
72
|
@children = value
|
90
73
|
|
91
|
-
@properties = @children.fetch(:properties, {})
|
92
|
-
@properties.compare_by_identity
|
74
|
+
@properties = @children.fetch(:properties, {}).compare_by_identity
|
93
75
|
@pattern_properties = @children.fetch(:pattern_properties, [])
|
94
76
|
|
95
|
-
@
|
77
|
+
@complex_properties_a = @properties.to_a.reject { _2.simple? }
|
78
|
+
@complex_pattern_properties_a = @pattern_properties.reject { _2.simple? }
|
96
79
|
|
97
80
|
@has_properties = @properties.any? || @pattern_properties.any?
|
98
81
|
|
99
|
-
return unless @
|
82
|
+
return unless @has_properties
|
100
83
|
|
101
84
|
if @pattern_properties.any?
|
102
85
|
@property_class = SymbolHash
|
103
86
|
else
|
104
87
|
invisible = @properties.select { |_k, v| v.invisible }.map(&:first)
|
105
|
-
@property_class =
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
private def build_caster
|
112
|
-
if @property_class
|
113
|
-
->(value) { value.is_a?(@property_class) ? value : @property_class.new(value.to_h) }
|
114
|
-
else
|
115
|
-
->(value) { value }
|
88
|
+
@property_class = LazyGraph.fetch_property_class(
|
89
|
+
path,
|
90
|
+
{ members: @properties.keys + (@debug && !parent ? [:DEBUG] : []),
|
91
|
+
invisible: invisible },
|
92
|
+
namespace: root.namespace
|
93
|
+
)
|
116
94
|
end
|
95
|
+
define_singleton_method(:cast, lambda { |val|
|
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
|
101
|
+
})
|
117
102
|
end
|
118
103
|
end
|
119
104
|
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
|
data/lib/lazy_graph/node.rb
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
require 'debug'
|
2
1
|
# frozen_string_literal: true
|
3
2
|
|
4
3
|
require 'bigdecimal/util'
|
@@ -12,8 +11,19 @@ module LazyGraph
|
|
12
11
|
|
13
12
|
DIGIT_REGEXP = /^-?\d+$/
|
14
13
|
SAFE_TOKEN_REGEXP = /^[A-Za-z][A-Za-z0-9]*$/
|
15
|
-
PROPERTY_CLASSES =
|
16
|
-
|
14
|
+
PROPERTY_CLASSES = {}
|
15
|
+
UNIQUE_NAME_COUNTER = Hash.new(0)
|
16
|
+
|
17
|
+
def self.fetch_property_class(name, members, namespace: nil)
|
18
|
+
namespace ||= LazyGraph
|
19
|
+
PROPERTY_CLASSES[members] ||= begin
|
20
|
+
name = name.to_s.capitalize.gsub(/(\.|_)([a-zA-Z])/) do |m|
|
21
|
+
m[1].upcase
|
22
|
+
end.gsub(/[^a-zA-Z]/, '').then { |n| n.length > 0 ? n : 'NodeProps' }
|
23
|
+
index = UNIQUE_NAME_COUNTER[[name, namespace]] += 1
|
24
|
+
full_name = "#{name}#{index > 1 ? index : ''}"
|
25
|
+
namespace.const_set(full_name, NodeProperties.build(**members))
|
26
|
+
end
|
17
27
|
end
|
18
28
|
|
19
29
|
# Class: Node
|
@@ -39,27 +49,48 @@ module LazyGraph
|
|
39
49
|
include DerivedRules
|
40
50
|
attr_accessor :name, :path, :type, :derived, :depth, :parent, :root, :invisible
|
41
51
|
attr_accessor :children
|
42
|
-
attr_reader :is_object
|
52
|
+
attr_reader :is_object, :namespace
|
53
|
+
|
54
|
+
def simple? = @simple
|
43
55
|
|
44
|
-
def initialize(name, path, node, parent, debug: false, helpers: nil)
|
56
|
+
def initialize(name, path, node, parent, debug: false, helpers: nil, namespace: nil)
|
45
57
|
@name = name
|
46
58
|
@path = path
|
47
59
|
@parent = parent
|
48
60
|
@debug = debug
|
49
61
|
@depth = parent ? parent.depth + 1 : 0
|
50
62
|
@root = parent ? parent.root : self
|
63
|
+
@rule = node[:rule]
|
64
|
+
@rule_location = node[:rule_location]
|
51
65
|
@type = node[:type]
|
52
|
-
@
|
66
|
+
@validate_presence = node[:validate_presence]
|
67
|
+
@helpers = helpers
|
68
|
+
@invisible = debug.eql?(true) ? false : node[:invisible]
|
53
69
|
@visited = {}.compare_by_identity
|
70
|
+
@namespace = namespace
|
71
|
+
|
54
72
|
instance_variable_set("@is_#{@type}", true)
|
55
73
|
define_singleton_method(:cast, build_caster)
|
74
|
+
define_singleton_method(:trace!, proc { |*| }) unless @debug
|
75
|
+
|
56
76
|
define_missing_value_proc!
|
57
77
|
|
58
78
|
@has_default = node.key?(:default)
|
59
79
|
@default = @has_default ? cast(node[:default]) : MissingValue { @name }
|
60
|
-
@resolution_stack = []
|
61
80
|
|
62
|
-
|
81
|
+
# Simple nodes are not a container type, and do not have rule or default
|
82
|
+
@simple = !(%i[object array date time timestamp decimal].include?(@type) || node[:rule] || @has_default)
|
83
|
+
end
|
84
|
+
|
85
|
+
def build_derived_inputs!
|
86
|
+
build_derived_inputs(@rule, @helpers) if @rule
|
87
|
+
return unless @children
|
88
|
+
return @children.build_derived_inputs! if @children.is_a?(Node)
|
89
|
+
|
90
|
+
@children[:properties]&.each_value(&:build_derived_inputs!)
|
91
|
+
@children[:pattern_properties]&.each do |(_, node)|
|
92
|
+
node.build_derived_inputs!
|
93
|
+
end
|
63
94
|
end
|
64
95
|
|
65
96
|
def define_missing_value_proc!
|
@@ -69,11 +100,31 @@ module LazyGraph
|
|
69
100
|
)
|
70
101
|
end
|
71
102
|
|
72
|
-
|
103
|
+
def fetch_and_resolve(path, input, segment, stack_memory, preserve_keys = nil)
|
104
|
+
item = fetch_item(input, segment, stack_memory)
|
105
|
+
unless @simple || item.is_a?(MissingValue)
|
106
|
+
item = resolve(
|
107
|
+
path,
|
108
|
+
stack_memory.push(item, segment)
|
109
|
+
)
|
110
|
+
end
|
111
|
+
|
112
|
+
item = cast(item) if @simple
|
113
|
+
|
114
|
+
preserve_keys ? preserve_keys[segment] = item : item
|
115
|
+
end
|
116
|
+
|
117
|
+
def build_caster
|
73
118
|
if @is_decimal
|
74
119
|
->(value) { value.is_a?(BigDecimal) ? value : value.to_d }
|
75
120
|
elsif @is_date
|
76
|
-
|
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
|
+
}
|
77
128
|
elsif @is_boolean
|
78
129
|
lambda do |value|
|
79
130
|
if value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
@@ -93,6 +144,8 @@ module LazyGraph
|
|
93
144
|
value
|
94
145
|
end
|
95
146
|
end
|
147
|
+
elsif @is_string
|
148
|
+
lambda(&:to_s)
|
96
149
|
else
|
97
150
|
->(value) { value }
|
98
151
|
end
|
@@ -100,9 +153,9 @@ module LazyGraph
|
|
100
153
|
|
101
154
|
def clear_visits!
|
102
155
|
@visited.clear
|
103
|
-
@resolution_stack
|
104
|
-
@path_cache
|
105
|
-
@resolvers
|
156
|
+
@resolution_stack&.clear
|
157
|
+
@path_cache&.clear
|
158
|
+
@resolvers&.clear
|
106
159
|
|
107
160
|
return unless @children
|
108
161
|
return @children.clear_visits! if @children.is_a?(Node)
|
@@ -113,10 +166,6 @@ module LazyGraph
|
|
113
166
|
end
|
114
167
|
end
|
115
168
|
|
116
|
-
# When we assign children to a node, we preemptively extract the properties, and pattern properties
|
117
|
-
# in both hash and array form. This micro-optimization pays off when we resolve values in the graph at
|
118
|
-
# very high frequency.
|
119
|
-
|
120
169
|
def resolve(
|
121
170
|
path,
|
122
171
|
stack_memory,
|
@@ -142,7 +191,7 @@ module LazyGraph
|
|
142
191
|
when Array then :array
|
143
192
|
end
|
144
193
|
node.children = Node.new(:items, :"#{path}.#{key}[].items", { type: child_type }, node)
|
145
|
-
node.children.children = { properties: {}, pattern_properties: [] } if child_type
|
194
|
+
node.children.children = { properties: {}, pattern_properties: [] } if child_type.equal? :object
|
146
195
|
node
|
147
196
|
else
|
148
197
|
Node.new(key, :"#{path}.#{key}", {}, self)
|
@@ -161,43 +210,25 @@ module LazyGraph
|
|
161
210
|
end
|
162
211
|
end
|
163
212
|
|
164
|
-
def resolve_input(stack_memory, path, key)
|
165
|
-
input_id = key.object_id >> 2 ^ stack_memory.shifted_id
|
166
|
-
if @resolution_stack.include?(input_id)
|
167
|
-
if @debug
|
168
|
-
stack_memory.log_debug(
|
169
|
-
property: "#{stack_memory}.#{key}",
|
170
|
-
exception: 'Infinite Recursion Detected during dependency resolution'
|
171
|
-
)
|
172
|
-
end
|
173
|
-
return MissingValue { "Infinite Recursion in #{stack_memory} => #{path.to_path_str}" }
|
174
|
-
end
|
175
|
-
|
176
|
-
@resolution_stack << (input_id)
|
177
|
-
first_segment = path.segment.part
|
178
|
-
|
179
|
-
resolver_node = @resolvers[first_segment] ||= (first_segment == key ? parent.parent : @parent).find_resolver_for(first_segment)
|
180
|
-
|
181
|
-
if resolver_node
|
182
|
-
input_frame_pointer = stack_memory.ptr_at(resolver_node.depth)
|
183
|
-
resolver_node.resolve(
|
184
|
-
first_segment == :'$' ? path.next : path,
|
185
|
-
input_frame_pointer,
|
186
|
-
nil
|
187
|
-
)
|
188
|
-
else
|
189
|
-
MissingValue { path.to_path_str }
|
190
|
-
end
|
191
|
-
ensure
|
192
|
-
@resolution_stack.pop
|
193
|
-
end
|
194
|
-
|
195
213
|
def ancestors
|
196
214
|
@ancestors ||= [self, *(@parent ? @parent.ancestors : [])]
|
197
215
|
end
|
198
216
|
|
199
217
|
def find_resolver_for(segment)
|
200
|
-
segment
|
218
|
+
segment.equal?(:'$') ? root : @parent&.find_resolver_for(segment)
|
219
|
+
end
|
220
|
+
|
221
|
+
def resolve_relative_input(stack_memory, path)
|
222
|
+
input_frame_pointer = path.absolute? ? stack_memory.root : stack_memory.ptr_at(depth - 1)
|
223
|
+
input_frame_pointer.recursion_depth += 1
|
224
|
+
|
225
|
+
return cast(input_frame_pointer.frame[path.first_path_segment.part]) if @simple
|
226
|
+
|
227
|
+
fetch_and_resolve(
|
228
|
+
path.absolute? ? path.next.next : path.next, input_frame_pointer.frame, path.first_path_segment.part, input_frame_pointer
|
229
|
+
)
|
230
|
+
ensure
|
231
|
+
input_frame_pointer.recursion_depth -= 1
|
201
232
|
end
|
202
233
|
|
203
234
|
def fetch_item(input, key, stack)
|
@@ -217,21 +248,35 @@ module LazyGraph
|
|
217
248
|
|
218
249
|
return input[key] = @default unless derived
|
219
250
|
|
220
|
-
if
|
221
|
-
|
222
|
-
|
223
|
-
|
251
|
+
if stack.recursion_depth >= 8
|
252
|
+
input_id = key.object_id >> 2 ^ input.object_id << 28
|
253
|
+
if @resolution_stack.key?(input_id)
|
254
|
+
trace!(stack, exception: 'Infinite Recursion Detected during dependency resolution') do
|
255
|
+
{ output: :"#{stack}.#{key}" }
|
256
|
+
end
|
257
|
+
return MissingValue { "Infinite Recursion in #{stack} => #{key}" }
|
258
|
+
end
|
259
|
+
@resolution_stack[input_id] = true
|
224
260
|
end
|
261
|
+
|
262
|
+
@copy_input ? copy_item!(input, key, stack, @inputs.first) : derive_item!(input, key, stack)
|
263
|
+
ensure
|
264
|
+
@resolution_stack.delete(input_id) if input_id
|
225
265
|
end
|
226
266
|
|
227
|
-
def copy_item!(input, key, stack, (path, _i,
|
228
|
-
|
229
|
-
|
267
|
+
def copy_item!(input, key, stack, (path, resolver, _i, segments))
|
268
|
+
missing_value = resolver ? nil : MissingValue { key }
|
269
|
+
if resolver && segments
|
230
270
|
parts = path.parts.dup
|
231
271
|
parts_identity = path.identity
|
232
|
-
|
233
|
-
|
234
|
-
|
272
|
+
segments.each do |index, resolver|
|
273
|
+
break missing_value = MissingValue { key } unless resolver
|
274
|
+
|
275
|
+
part = resolver.resolve_relative_input(stack, parts[index].options.first)
|
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
|
235
280
|
|
236
281
|
part_sym = part.to_s.to_sym
|
237
282
|
parts_identity ^= part_sym.object_id << index
|
@@ -240,28 +285,30 @@ module LazyGraph
|
|
240
285
|
path = @path_cache[parts_identity] ||= PathParser::Path.new(parts: parts) unless missing_value
|
241
286
|
end
|
242
287
|
|
243
|
-
result = missing_value || cast(
|
288
|
+
result = missing_value || cast(resolver.resolve_relative_input(stack, path))
|
244
289
|
|
245
|
-
if
|
246
|
-
stack
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
calc: @src
|
251
|
-
)
|
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
|
252
295
|
end
|
253
|
-
input[key] = result.nil? ? MissingValue { key } : result
|
254
296
|
end
|
255
297
|
|
256
298
|
def derive_item!(input, key, stack)
|
257
|
-
@inputs.each do |path, i,
|
258
|
-
if
|
299
|
+
@inputs.each do |path, resolver, i, segments|
|
300
|
+
if segments
|
259
301
|
missing_value = nil
|
260
302
|
parts = path.parts.dup
|
261
303
|
parts_identity = path.identity
|
262
|
-
|
263
|
-
|
264
|
-
|
304
|
+
segments.each do |index, resolver|
|
305
|
+
break missing_value = MissingValue { key } unless resolver
|
306
|
+
|
307
|
+
part = resolver.resolve_relative_input(stack, parts[index].options.first)
|
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
|
265
312
|
|
266
313
|
part_sym = part.to_s.to_sym
|
267
314
|
parts_identity ^= part_sym.object_id << (index * 8)
|
@@ -269,8 +316,28 @@ module LazyGraph
|
|
269
316
|
end
|
270
317
|
path = @path_cache[parts_identity] ||= PathParser::Path.new(parts: parts) unless missing_value
|
271
318
|
end
|
272
|
-
result =
|
273
|
-
|
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
|
274
341
|
end
|
275
342
|
|
276
343
|
@node_context[:itself] = input
|
@@ -285,12 +352,21 @@ module LazyGraph
|
|
285
352
|
if conditions_passed
|
286
353
|
output = begin
|
287
354
|
cast(@fixed_result || @node_context.process!)
|
288
|
-
rescue
|
355
|
+
rescue AbortError, ValidationError => e
|
289
356
|
raise e
|
290
357
|
rescue StandardError => e
|
291
358
|
ex = e
|
292
359
|
LazyGraph.logger.error(e)
|
293
|
-
LazyGraph.logger.error(e.backtrace.
|
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
|
+
|
294
370
|
MissingValue { "#{key} raised exception: #{e.message}" }
|
295
371
|
end
|
296
372
|
|
@@ -299,24 +375,37 @@ module LazyGraph
|
|
299
375
|
MissingValue { key }
|
300
376
|
end
|
301
377
|
|
302
|
-
if
|
303
|
-
stack
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
end }
|
313
|
-
else
|
314
|
-
{}
|
315
|
-
end
|
316
|
-
)
|
317
|
-
)
|
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
|
318
388
|
end
|
389
|
+
|
319
390
|
result
|
320
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
|
321
410
|
end
|
322
411
|
end
|
@@ -10,25 +10,33 @@ module LazyGraph
|
|
10
10
|
Path = Struct.new(:parts, keyword_init: true) do
|
11
11
|
def next = @next ||= parts.length <= 1 ? Path::BLANK : Path.new(parts: parts[1..])
|
12
12
|
def empty? = @empty ||= parts.empty?
|
13
|
+
def length = @length ||= parts.length
|
13
14
|
def segment = @segment ||= parts&.[](0)
|
14
|
-
def
|
15
|
+
def absolute? = instance_variable_defined?(:@absolute) ? @absolute : (@absolute = segment&.part.equal?(:'$'))
|
16
|
+
def index? = @index ||= !empty? && segment&.index?
|
15
17
|
def identity = @identity ||= parts&.each_with_index&.reduce(0) { |acc, (p, i)| acc ^ (p.object_id) << (i * 8) }
|
16
18
|
def map(&block) = empty? ? self : Path.new(parts: parts.map(&block))
|
17
19
|
def shifted_id = @shifted_id ||= object_id << 28
|
20
|
+
def first_path_segment = @first_path_segment ||= absolute? ? self.next.segment : segment
|
18
21
|
|
19
22
|
def merge(other)
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
end
|
23
|
+
if other.empty?
|
24
|
+
self
|
25
|
+
else
|
26
|
+
empty? ? other : Path.new(parts: parts + other.parts)
|
27
|
+
end
|
26
28
|
end
|
27
29
|
|
28
30
|
def to_path_str
|
29
31
|
@to_path_str ||= create_path_str
|
30
32
|
end
|
31
33
|
|
34
|
+
def ==(other)
|
35
|
+
return parts == other if other.is_a?(Array)
|
36
|
+
|
37
|
+
super
|
38
|
+
end
|
39
|
+
|
32
40
|
private
|
33
41
|
|
34
42
|
def create_path_str
|
@@ -8,6 +8,14 @@ module LazyGraph
|
|
8
8
|
def index?
|
9
9
|
@index ||= part =~ INDEX_REGEXP
|
10
10
|
end
|
11
|
+
|
12
|
+
def ==(other)
|
13
|
+
return part == other.to_sym if other.is_a?(String)
|
14
|
+
return part == other if other.is_a?(Symbol)
|
15
|
+
return part == other if other.is_a?(Array)
|
16
|
+
|
17
|
+
super
|
18
|
+
end
|
11
19
|
end
|
12
20
|
end
|
13
21
|
end
|
@@ -19,7 +19,7 @@ module LazyGraph
|
|
19
19
|
require_relative 'path_parser/path_part'
|
20
20
|
# This module is responsible for parsing complex path strings into structured components.
|
21
21
|
# Public class method to parse the path string
|
22
|
-
def self.parse(path, strip_root
|
22
|
+
def self.parse(path, strip_root: false)
|
23
23
|
return Path::BLANK if path.nil? || path.empty?
|
24
24
|
|
25
25
|
start = strip_root && path.to_s.start_with?('$.') ? 2 : 0
|