lazy_graph 0.1.3 → 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.
- 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
|