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.
@@ -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({}.tap(&:compare_by_identity)) do |part, object|
22
- resolve(part.merge(path.next), stack_memory, nil, preserve_keys: object)
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
- @properties_a.each do |key, node|
28
- item = node.fetch_item(input, key, stack_memory)
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 @pattern_properties.any? && input.keys.length > @properties_a.length
32
- input.each_key do |key|
33
- node = !@properties[key] && @pattern_properties.find { |(pattern, _value)| pattern.match?(key) }&.last
34
- item = node.fetch_item(input, key, stack_memory)
35
- node.resolve(path.next, stack_memory.push(item, key))
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
- item = prop.fetch_item(input, segment, stack_memory)
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
- item = prop.fetch_item(input, segment, stack_memory)
57
- value = prop.resolve(
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
- item = prop.fetch_item(input, segment, stack_memory)
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
- self
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
- @properties_a = @properties.to_a
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 @properties.any? || @pattern_properties.any?
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 = PROPERTY_CLASSES[{ members: @properties.keys + (@debug && !parent ? [:DEBUG] : []),
106
- invisible: invisible }]
107
- end
108
- define_singleton_method(:cast, build_caster)
109
- end
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
@@ -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 = Hash.new do |h, members|
16
- h[members] = LazyGraph.const_set("NodeProperties#{h.hash.abs}", NodeProperties.build(**members))
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
- @invisible = debug ? false : node[:invisible]
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
- build_derived_inputs(node[:rule], helpers) if node[:rule]
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
- private def build_caster
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
- ->(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
+ }
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.clear
104
- @path_cache = {}.clear
105
- @resolvers = {}.clear
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 == :object
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 == :'$' ? root : @parent&.find_resolver_for(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 @copy_input
221
- copy_item!(input, key, stack, @inputs.first)
222
- else
223
- derive_item!(input, key, stack)
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, segment_indexes))
228
- if segment_indexes
229
- missing_value = nil
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
- segment_indexes.each do |index|
233
- part = resolve_input(stack, parts[index].options.first, key)
234
- break missing_value = part if part.is_a?(MissingValue)
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(resolve_input(stack, path, key))
288
+ result = missing_value || cast(resolver.resolve_relative_input(stack, path))
244
289
 
245
- if @debug
246
- stack.log_debug(
247
- output: :"#{stack}.#{key}",
248
- result: result,
249
- inputs: @node_context.to_h.except(:itself, :stack_ptr),
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, segment_indexes|
258
- if segment_indexes
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
- segment_indexes.each do |index|
263
- part = resolve_input(stack, parts[index].options.first, key)
264
- break missing_value = part if part.is_a?(MissingValue)
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 = missing_value || resolve_input(stack, path, key)
273
- @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
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 LazyGraph::AbortError => e
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.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
+
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 @debug
303
- stack.log_debug(
304
- output: :"#{stack}.#{key}",
305
- result: HashUtils.deep_dup(result),
306
- inputs: @node_context.to_h.except(:itself, :stack_ptr).transform_keys { |k| @input_mapper&.[](k) || k },
307
- calc: @src,
308
- **(@conditions ? { conditions: @conditions } : {}),
309
- **(if ex
310
- { exception: ex, backtrace: ex.backtrace.take_while do |line|
311
- !line.include?('lazy_graph/node.rb')
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 index? = @index ||= !empty? && segment&.index?
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
- (@merged ||= {})[other] ||= \
21
- if other.empty?
22
- self
23
- else
24
- empty? ? other : Path.new(parts: parts + other.parts)
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
@@ -7,6 +7,12 @@ module LazyGraph
7
7
  def index?
8
8
  @index ||= options.all?(&:index?)
9
9
  end
10
+
11
+ def ==(other)
12
+ return options == other if other.is_a?(Array)
13
+
14
+ super
15
+ end
10
16
  end
11
17
  end
12
18
  end
@@ -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 = false)
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