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.
@@ -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