lazy_graph 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rubocop.yml +81 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +562 -0
- data/Rakefile +4 -0
- data/examples/performance_tests.rb +117 -0
- data/lib/lazy_graph/builder/dsl.rb +315 -0
- data/lib/lazy_graph/builder.rb +138 -0
- data/lib/lazy_graph/builder_group.rb +57 -0
- data/lib/lazy_graph/context.rb +60 -0
- data/lib/lazy_graph/graph.rb +73 -0
- data/lib/lazy_graph/hash_utils.rb +87 -0
- data/lib/lazy_graph/lazy-graph.json +148 -0
- data/lib/lazy_graph/missing_value.rb +26 -0
- data/lib/lazy_graph/node/array_node.rb +67 -0
- data/lib/lazy_graph/node/derived_rules.rb +196 -0
- data/lib/lazy_graph/node/node_properties.rb +64 -0
- data/lib/lazy_graph/node/object_node.rb +113 -0
- data/lib/lazy_graph/node.rb +316 -0
- data/lib/lazy_graph/path_parser/path.rb +46 -0
- data/lib/lazy_graph/path_parser/path_group.rb +12 -0
- data/lib/lazy_graph/path_parser/path_part.rb +13 -0
- data/lib/lazy_graph/path_parser.rb +211 -0
- data/lib/lazy_graph/server.rb +86 -0
- data/lib/lazy_graph/stack_pointer.rb +56 -0
- data/lib/lazy_graph/version.rb +5 -0
- data/lib/lazy_graph.rb +32 -0
- data/logo.png +0 -0
- metadata +200 -0
@@ -0,0 +1,113 @@
|
|
1
|
+
module LazyGraph
|
2
|
+
class ObjectNode < Node
|
3
|
+
# An object supports the following types of path resolutions.
|
4
|
+
# 1. Property name: obj.property => value
|
5
|
+
# 2. Property name group: obj[property1, property2] => { property1: value1, property2: value2 }
|
6
|
+
# 3. All [*]
|
7
|
+
def resolve(
|
8
|
+
path,
|
9
|
+
stack_memory,
|
10
|
+
should_recycle = stack_memory,
|
11
|
+
preserve_keys: false
|
12
|
+
)
|
13
|
+
input = stack_memory.frame
|
14
|
+
@visited[input.object_id ^ (path.object_id << 8)] ||= begin
|
15
|
+
if (path_segment = path.segment).is_a?(PathParser::PathGroup)
|
16
|
+
return path_segment.options.each_with_object({}.tap(&:compare_by_identity)) do |part, object|
|
17
|
+
resolve(part.merge(path.next), stack_memory, nil, preserve_keys: object)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
if !segment = path_segment&.part
|
22
|
+
@properties_a.each do |key, node|
|
23
|
+
item = node.fetch_item(input, key, stack_memory)
|
24
|
+
node.resolve(path.next, stack_memory.push(item, key))
|
25
|
+
end
|
26
|
+
if @pattern_properties_a.any? && input.keys.length > @properties_a.length
|
27
|
+
input.each_key do |key|
|
28
|
+
node = !@properties[key] && @pattern_properties_a.find { |pattern, _value| pattern.match?(key) }&.last
|
29
|
+
item = node.fetch_item(input, key, stack_memory)
|
30
|
+
node.resolve(path.next, stack_memory.push(item, key))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
input
|
34
|
+
elsif (prop = @properties[segment])
|
35
|
+
item = prop.fetch_item(input, segment, stack_memory)
|
36
|
+
value = prop.resolve(
|
37
|
+
path.next, stack_memory.push(item, segment)
|
38
|
+
)
|
39
|
+
preserve_keys ? preserve_keys[segment] = value : value
|
40
|
+
elsif segment == :*
|
41
|
+
# rubocop:disable
|
42
|
+
(input.keys | @properties_a.map(&:first)).each do |key|
|
43
|
+
next unless (node = @properties[key] || @pattern_properties_a.find do |pattern, _value|
|
44
|
+
pattern.match?(key)
|
45
|
+
end&.last)
|
46
|
+
|
47
|
+
item = node.fetch_item(input, key, stack_memory)
|
48
|
+
preserve_keys[key] = node.resolve(path.next, stack_memory.push(item, key))
|
49
|
+
end
|
50
|
+
elsif (_, prop = @pattern_properties_a.find { |key, _val| key.match?(segment) })
|
51
|
+
item = prop.fetch_item(input, segment, stack_memory)
|
52
|
+
value = prop.resolve(
|
53
|
+
path.next, stack_memory.push(item, segment)
|
54
|
+
)
|
55
|
+
preserve_keys ? preserve_keys[segment] = value : value
|
56
|
+
elsif input.key?(segment)
|
57
|
+
prop = @properties[segment] = lazy_init_node!(input[segment], segment)
|
58
|
+
@properties_a = @properties.to_a
|
59
|
+
item = prop.fetch_item(input, segment, stack_memory)
|
60
|
+
value = prop.resolve(
|
61
|
+
path.next, stack_memory.push(item, segment)
|
62
|
+
)
|
63
|
+
preserve_keys ? preserve_keys[segment] = value : value
|
64
|
+
else
|
65
|
+
value = MissingValue()
|
66
|
+
preserve_keys ? preserve_keys[segment] = value : value
|
67
|
+
end
|
68
|
+
end
|
69
|
+
ensure
|
70
|
+
should_recycle&.recycle!
|
71
|
+
end
|
72
|
+
|
73
|
+
def find_resolver_for(segment)
|
74
|
+
if segment == :'$'
|
75
|
+
root
|
76
|
+
elsif @properties.key?(segment)
|
77
|
+
self
|
78
|
+
else
|
79
|
+
@parent&.find_resolver_for(segment)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def children=(value)
|
84
|
+
@children = value
|
85
|
+
|
86
|
+
@properties = @children.fetch(:properties, {})
|
87
|
+
@properties.compare_by_identity
|
88
|
+
@pattern_properties = @children.fetch(:pattern_properties, {})
|
89
|
+
|
90
|
+
@properties_a = @properties.to_a
|
91
|
+
@pattern_properties_a = @pattern_properties.to_a
|
92
|
+
|
93
|
+
@has_properties = @properties.any? || @pattern_properties.any?
|
94
|
+
return if @pattern_properties.any?
|
95
|
+
return unless @properties.any?
|
96
|
+
|
97
|
+
invisible = @properties.select { |_k, v| v.invisible }.map(&:first)
|
98
|
+
@property_class = PROPERTY_CLASSES[{ members: @properties.keys + (@debug && !parent ? [:DEBUG] : []),
|
99
|
+
invisible: invisible }]
|
100
|
+
end
|
101
|
+
|
102
|
+
def cast(value)
|
103
|
+
if !@property_class && value.is_a?(Hash)
|
104
|
+
value.default_proc = ->(h, k) { k.is_a?(Symbol) ? nil : h[k.to_s.to_sym] }
|
105
|
+
value.compare_by_identity
|
106
|
+
elsif @property_class && !value.is_a?(@property_class)
|
107
|
+
@property_class.new(value.to_h)
|
108
|
+
else
|
109
|
+
value
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,316 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bigdecimal/util'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
module LazyGraph
|
7
|
+
require_relative 'node/derived_rules'
|
8
|
+
require_relative 'node/array_node'
|
9
|
+
require_relative 'node/object_node'
|
10
|
+
require_relative 'node/node_properties'
|
11
|
+
|
12
|
+
DIGIT_REGEXP = /^-?\d+$/
|
13
|
+
SAFE_TOKEN_REGEXP = /^[A-Za-z][A-Za-z0-9]*$/
|
14
|
+
PROPERTY_CLASSES = Hash.new do |h, members|
|
15
|
+
h[members] = LazyGraph.const_set("NodeProperties#{h.hash.abs}", NodeProperties.build(**members))
|
16
|
+
end
|
17
|
+
|
18
|
+
# Class: Node
|
19
|
+
# Represents A single Node within our LazyGraph structure
|
20
|
+
# A node is a logical position with a graph structure.
|
21
|
+
# The node might capture knowledge about how to derived values at its position
|
22
|
+
# if a value is not provided.
|
23
|
+
# This can be in the form of a default value or a derivation rule.
|
24
|
+
#
|
25
|
+
# This class is heavily optimized to resolve values in a graph structure
|
26
|
+
# with as little overhead as possible. (Note heavy use of ivars,
|
27
|
+
# and minimal method calls in the recursive resolve method).
|
28
|
+
#
|
29
|
+
# Nodes support (non-circular) recursive resolution of values, i.e.
|
30
|
+
# if a node depends on the output of several other nodes in the graph,
|
31
|
+
# it will resolve those nodes first before resolving itself.
|
32
|
+
#
|
33
|
+
# Node resolution maintains a full stack, so that values can be resolved relative to the position
|
34
|
+
# of the node itself.
|
35
|
+
#
|
36
|
+
|
37
|
+
class Node
|
38
|
+
include DerivedRules
|
39
|
+
attr_accessor :name, :path, :type, :derived, :depth, :parent, :root, :invisible
|
40
|
+
attr_accessor :children
|
41
|
+
attr_reader :is_object
|
42
|
+
|
43
|
+
def initialize(name, path, node, parent, debug: false, helpers: nil)
|
44
|
+
@name = name
|
45
|
+
@path = path
|
46
|
+
@parent = parent
|
47
|
+
@debug = debug
|
48
|
+
@depth = parent ? parent.depth + 1 : 0
|
49
|
+
@root = parent ? parent.root : self
|
50
|
+
@type = node[:type]
|
51
|
+
@invisible = node[:invisible]
|
52
|
+
@visited = {}.compare_by_identity
|
53
|
+
|
54
|
+
define_missing_value_proc!
|
55
|
+
|
56
|
+
@has_default = node.key?(:default)
|
57
|
+
@default = @has_default ? node[:default] : MissingValue { @name }
|
58
|
+
@resolution_stack = []
|
59
|
+
|
60
|
+
instance_variable_set("@is_#{@type}", true)
|
61
|
+
build_derived_inputs(node[:rule], helpers) if node[:rule]
|
62
|
+
end
|
63
|
+
|
64
|
+
def define_missing_value_proc!
|
65
|
+
define_singleton_method(
|
66
|
+
:MissingValue,
|
67
|
+
@debug ? ->(&blk) { MissingValue.new(blk&.call || absolute_path) } : -> { MissingValue::BLANK }
|
68
|
+
)
|
69
|
+
end
|
70
|
+
|
71
|
+
def clear_visits!
|
72
|
+
@visited.clear
|
73
|
+
return unless @children
|
74
|
+
return @children.clear_visits! if @children.is_a?(Node)
|
75
|
+
|
76
|
+
@children[:properties]&.each do |_, node|
|
77
|
+
node.clear_visits!
|
78
|
+
end
|
79
|
+
|
80
|
+
@children[:pattern_properties]&.each do |_, node|
|
81
|
+
node.clear_visits!
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# When we assign children to a node, we preemptively extract the properties, and pattern properties
|
86
|
+
# in both hash and array form. This micro-optimization pays off when we resolve values in the graph at
|
87
|
+
# very high frequency.
|
88
|
+
|
89
|
+
def resolve(
|
90
|
+
path,
|
91
|
+
stack_memory,
|
92
|
+
should_recycle = stack_memory,
|
93
|
+
**
|
94
|
+
)
|
95
|
+
path.empty? ? stack_memory.frame : MissingValue()
|
96
|
+
ensure
|
97
|
+
should_recycle&.recycle!
|
98
|
+
end
|
99
|
+
|
100
|
+
def lazy_init_node!(input, key)
|
101
|
+
case input
|
102
|
+
when Hash
|
103
|
+
node = Node.new(key, "#{path}.#{key}", { type: :object }, self)
|
104
|
+
node.children = { properties: {}, pattern_properties: {} }
|
105
|
+
node
|
106
|
+
when Array
|
107
|
+
node = Node.new(key, :"#{path}.#{key}[]", { type: :array }, self)
|
108
|
+
child_type = \
|
109
|
+
case input.first
|
110
|
+
when Hash then :object
|
111
|
+
when Array then :array
|
112
|
+
end
|
113
|
+
node.children = Node.new(:items, :"#{path}.#{key}[].items", { type: child_type }, node)
|
114
|
+
node.children.children = { properties: {}, pattern_properties: {} } if child_type == :object
|
115
|
+
node
|
116
|
+
else
|
117
|
+
Node.new(key, :"#{path}.#{key}", {}, self)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def absolute_path
|
122
|
+
@absolute_path ||= begin
|
123
|
+
next_node = self
|
124
|
+
path = []
|
125
|
+
while next_node
|
126
|
+
path << next_node.name
|
127
|
+
next_node = next_node.parent
|
128
|
+
end
|
129
|
+
path.reverse.join('.')
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def resolve_input(stack_memory, path, key)
|
134
|
+
input_id = key.object_id ^ (stack_memory.object_id << 8)
|
135
|
+
if @resolution_stack.include?(input_id)
|
136
|
+
if @debug
|
137
|
+
stack_memory.log_debug(
|
138
|
+
property: "#{stack_memory}.#{key}",
|
139
|
+
exception: 'Infinite Recursion Detected during dependency resolution'
|
140
|
+
)
|
141
|
+
end
|
142
|
+
return MissingValue { "Infinite Recursion in #{stack_memory} => #{path.to_path_str}" }
|
143
|
+
end
|
144
|
+
|
145
|
+
@resolution_stack << (input_id)
|
146
|
+
first_segment = path.parts.first.part
|
147
|
+
resolver_node = @resolvers[first_segment] ||= (first_segment == key ? parent.parent : parent).find_resolver_for(first_segment)
|
148
|
+
|
149
|
+
if resolver_node
|
150
|
+
input_frame_pointer = stack_memory.ptr_at(resolver_node.depth)
|
151
|
+
resolver_node.resolve(
|
152
|
+
first_segment == :'$' ? path.next : path,
|
153
|
+
input_frame_pointer,
|
154
|
+
nil
|
155
|
+
)
|
156
|
+
else
|
157
|
+
MissingValue { path.to_path_str }
|
158
|
+
end
|
159
|
+
ensure
|
160
|
+
@resolution_stack.pop
|
161
|
+
end
|
162
|
+
|
163
|
+
def ancestors
|
164
|
+
@ancestors ||= [self, *(parent ? parent.ancestors : [])]
|
165
|
+
end
|
166
|
+
|
167
|
+
def find_resolver_for(segment)
|
168
|
+
segment == :'$' ? root : @parent&.find_resolver_for(segment)
|
169
|
+
end
|
170
|
+
|
171
|
+
def cast(value)
|
172
|
+
if @is_decimal
|
173
|
+
value.is_a?(BigDecimal) ? value : value.to_d
|
174
|
+
elsif @is_date
|
175
|
+
value.is_a?(String) ? Date.parse(value) : value
|
176
|
+
elsif @is_boolean
|
177
|
+
if value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
178
|
+
value
|
179
|
+
else
|
180
|
+
value.is_a?(MissingValue) ? false : !!value
|
181
|
+
end
|
182
|
+
elsif @is_timestamp
|
183
|
+
case value
|
184
|
+
when String then DateTime.parse(value).to_time
|
185
|
+
when Numeric then Time.at(value)
|
186
|
+
else value
|
187
|
+
end
|
188
|
+
else
|
189
|
+
value
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def fetch_item(input, key, stack)
|
194
|
+
return MissingValue { key } unless input
|
195
|
+
|
196
|
+
has_value = \
|
197
|
+
case input
|
198
|
+
when Array then input.length > key && input[key]
|
199
|
+
when Hash, Struct then input.key?(key) && !input[key].is_a?(MissingValue)
|
200
|
+
end
|
201
|
+
|
202
|
+
if has_value
|
203
|
+
value = input[key]
|
204
|
+
value = cast(value) if value || @is_boolean
|
205
|
+
return input[key] = value
|
206
|
+
end
|
207
|
+
|
208
|
+
return input[key] = @default unless derived
|
209
|
+
|
210
|
+
if @copy_input
|
211
|
+
copy_item!(input, key, stack, @inputs.first)
|
212
|
+
else
|
213
|
+
derive_item!(input, key, stack)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
def copy_item!(input, key, stack, (path, i, segment_indexes))
|
218
|
+
if segment_indexes
|
219
|
+
missing_value = nil
|
220
|
+
parts = path.parts.dup
|
221
|
+
parts_identity = path.identity
|
222
|
+
segment_indexes.each do |index|
|
223
|
+
part = resolve_input(stack, parts[index].options.first, key)
|
224
|
+
break missing_value = part if part.is_a?(MissingValue)
|
225
|
+
|
226
|
+
part_sym = part.to_s.to_sym
|
227
|
+
parts_identity ^= part_sym.object_id << index
|
228
|
+
parts[index] = @path_cache[part_sym] ||= PathParser::PathPart.new(part: part_sym)
|
229
|
+
end
|
230
|
+
path = @path_cache[parts_identity] ||= PathParser::Path.new(parts: parts) unless missing_value
|
231
|
+
end
|
232
|
+
|
233
|
+
result = missing_value || cast(resolve_input(stack, path, key))
|
234
|
+
|
235
|
+
if @debug
|
236
|
+
stack.log_debug(
|
237
|
+
output: :"#{stack}.#{key}",
|
238
|
+
result: result,
|
239
|
+
inputs: @node_context.to_h.except(:itself, :stack_ptr),
|
240
|
+
calc: @src
|
241
|
+
)
|
242
|
+
end
|
243
|
+
|
244
|
+
input[key] = result.nil? ? MissingValue { key } : result
|
245
|
+
end
|
246
|
+
|
247
|
+
def derive_item!(input, key, stack)
|
248
|
+
@inputs.each do |path, i, segment_indexes|
|
249
|
+
if segment_indexes
|
250
|
+
missing_value = nil
|
251
|
+
parts = path.parts.dup
|
252
|
+
parts_identity = path.identity
|
253
|
+
segment_indexes.each do |index|
|
254
|
+
part = resolve_input(stack, parts[index].options.first, key)
|
255
|
+
break missing_value = part if part.is_a?(MissingValue)
|
256
|
+
|
257
|
+
part_sym = part.to_s.to_sym
|
258
|
+
parts_identity ^= part_sym.object_id << index
|
259
|
+
parts[index] = @path_cache[part_sym] ||= PathParser::PathPart.new(part: part_sym)
|
260
|
+
end
|
261
|
+
path = @path_cache[parts_identity] ||= PathParser::Path.new(parts: parts) unless missing_value
|
262
|
+
end
|
263
|
+
|
264
|
+
result = missing_value || resolve_input(stack, path, key)
|
265
|
+
@node_context[i] = result.is_a?(MissingValue) ? nil : result
|
266
|
+
end
|
267
|
+
|
268
|
+
@node_context[:itself] = input
|
269
|
+
@node_context[:stack_ptr] = stack
|
270
|
+
|
271
|
+
conditions_passed = !@conditions || @conditions.all? do |field, allowed_value|
|
272
|
+
allowed_value.is_a?(Array) ? allowed_value.include?(@node_context[field]) : allowed_value == @node_context[field]
|
273
|
+
end
|
274
|
+
|
275
|
+
ex = nil
|
276
|
+
result = \
|
277
|
+
if conditions_passed
|
278
|
+
output = begin
|
279
|
+
cast(@node_context.process!)
|
280
|
+
rescue LazyGraph::AbortError => e
|
281
|
+
raise e
|
282
|
+
rescue StandardError => e
|
283
|
+
ex = e
|
284
|
+
MissingValue { "#{key} raised exception: #{e.message}" }
|
285
|
+
end
|
286
|
+
output = output.dup if @has_properties
|
287
|
+
input[key] = output.nil? ? MissingValue { key } : output
|
288
|
+
else
|
289
|
+
MissingValue { key }
|
290
|
+
end
|
291
|
+
|
292
|
+
if @debug
|
293
|
+
stack.log_debug(
|
294
|
+
output: :"#{stack}.#{key}",
|
295
|
+
result: result,
|
296
|
+
inputs: @node_context.to_h.except(:itself, :stack_ptr),
|
297
|
+
calc: @src,
|
298
|
+
**(@conditions ? { conditions: @conditions } : {}),
|
299
|
+
**(
|
300
|
+
if ex
|
301
|
+
{
|
302
|
+
exception: ex,
|
303
|
+
backtrace: ex.backtrace.take_while do |line|
|
304
|
+
!line.include?('lazy_graph/node.rb')
|
305
|
+
end
|
306
|
+
}
|
307
|
+
else
|
308
|
+
{}
|
309
|
+
end
|
310
|
+
)
|
311
|
+
)
|
312
|
+
end
|
313
|
+
result
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LazyGraph
|
4
|
+
module PathParser
|
5
|
+
# This module is responsible for parsing complex path strings into structured components.
|
6
|
+
|
7
|
+
# Path represents a structured component of a complex path string.
|
8
|
+
# It provides methods to navigate and manipulate the path parts.
|
9
|
+
|
10
|
+
Path = Struct.new(:parts, keyword_init: true) do
|
11
|
+
def next = @next ||= parts.length <= 1 ? Path::BLANK : Path.new(parts: parts[1..])
|
12
|
+
def empty? = @empty ||= parts.empty?
|
13
|
+
def segment = @segment ||= parts&.[](0)
|
14
|
+
def index? = @index ||= parts.any? && parts.first.index?
|
15
|
+
def identity = @identity ||= parts&.each_with_index&.reduce(0) { |acc, (p, i)| acc ^ (p.object_id) << (i * 4) }
|
16
|
+
def map(&block) = empty? ? self : Path.new(parts: parts.map(&block))
|
17
|
+
|
18
|
+
def merge(other)
|
19
|
+
(@merged ||= {})[other] ||= \
|
20
|
+
if other.empty?
|
21
|
+
self
|
22
|
+
else
|
23
|
+
empty? ? other : Path.new(parts: parts + other.parts)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_path_str
|
28
|
+
@to_path_str ||= create_path_str
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def create_path_str
|
34
|
+
parts.inject('$') do |path_str, part|
|
35
|
+
path_str + \
|
36
|
+
if part.is_a?(PathPart)
|
37
|
+
".#{part.part}"
|
38
|
+
else
|
39
|
+
"[#{part.options.map(&:to_path_str).join(',').delete_prefix('$.')}]"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
Path::BLANK = Path.new(parts: [])
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LazyGraph
|
4
|
+
module PathParser
|
5
|
+
# Represents a group of paths with a list of options, which must all be resolved.
|
6
|
+
PathGroup = Struct.new(:options, keyword_init: true) do
|
7
|
+
def index?
|
8
|
+
@index ||= options.all?(&:index?)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LazyGraph
|
4
|
+
module PathParser
|
5
|
+
INDEX_REGEXP = /\A-?\d+\z/
|
6
|
+
# Represents a single part of a path.
|
7
|
+
PathPart = Struct.new(:part, keyword_init: true) do
|
8
|
+
def index?
|
9
|
+
@index ||= part =~ INDEX_REGEXP
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|