metaractor-sycamore 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,266 @@
1
+ require 'forwardable'
2
+
3
+ module Sycamore
4
+
5
+ ##
6
+ # A compact, immutable representation of Tree paths, i.e. node sequences.
7
+ #
8
+ # This class is optimized for its usage in {Tree#each_path}, where it
9
+ # can efficiently represent the whole tree as a set of paths by sharing the
10
+ # parent paths.
11
+ # It is not intended to be instantiated by the user.
12
+ #
13
+ # @example
14
+ # tree = Tree[foo: [:bar, :baz]]
15
+ # path1, path2 = tree.paths.to_a
16
+ # path1 == Sycamore::Path[:foo, :bar] # => true
17
+ # path2 == Sycamore::Path[:foo, :baz] # => true
18
+ # path1.parent.equal? path2.parent # => true
19
+ #
20
+ # @todo Measure the performance and memory consumption in comparison with a
21
+ # pure Array-based implementation (where tree nodes are duplicated), esp. in
22
+ # the most common use case of property-value structures.
23
+ #
24
+ class Path
25
+ include Enumerable
26
+ extend Forwardable
27
+
28
+ attr_reader :node, :parent
29
+
30
+ ########################################################################
31
+ # @group Construction
32
+ ########################################################################
33
+
34
+ ##
35
+ # @private
36
+ #
37
+ def initialize(parent, node)
38
+ @parent, @node = parent, node
39
+ end
40
+
41
+ ##
42
+ # @return the root of all Paths
43
+ #
44
+ def self.root
45
+ ROOT
46
+ end
47
+
48
+ ##
49
+ # Creates a new path.
50
+ #
51
+ # Depending on whether the first argument is a {Path}, the new Path is
52
+ # {#branch}ed from this path or the {root}.
53
+ #
54
+ # @overload of(path, nodes)
55
+ # @param path [Path] the path from which should be {#branch}ed
56
+ # @param nodes [nodes]
57
+ # @return [Path] the {#branch}ed path from the given path, with the given nodes expanded
58
+ #
59
+ # @overload of(nodes)
60
+ # @param nodes [nodes]
61
+ # @return [Path] the {#branch}ed path from the {root}, with the given nodes
62
+ #
63
+ def self.of(*args)
64
+ if (parent = args.first).is_a? Path
65
+ parent.branch(*args[1..-1])
66
+ else
67
+ root.branch(*args)
68
+ end
69
+ end
70
+
71
+ class << self
72
+ private :new # disable Path.new
73
+
74
+ alias [] of
75
+ end
76
+
77
+ ########################################################################
78
+ # @group Elements
79
+ ########################################################################
80
+
81
+ def_delegators :to_a, :[], :fetch
82
+
83
+ ##
84
+ # Returns a new path based on this path, but with the given nodes extended.
85
+ #
86
+ # @param nodes [nodes] an arbitrary number of nodes
87
+ # @return [Path]
88
+ #
89
+ # @raise [InvalidNode] if one or more of the given nodes is an Enumerable
90
+ #
91
+ # @example
92
+ # path = Sycamore::Path[:foo, :bar]
93
+ # path.branch(:baz, :qux) ==
94
+ # Sycamore::Path[:foo, :bar, :baz, :qux] # => true
95
+ # path / :baz / :qux ==
96
+ # Sycamore::Path[:foo, :bar, :baz, :qux] # => true
97
+ #
98
+ def branch(*nodes)
99
+ return branch(*nodes.first) if nodes.size == 1 and nodes.first.is_a? Enumerable
100
+
101
+ parent = self
102
+ nodes.each do |node|
103
+ raise InvalidNode, "#{node} in Path #{nodes.inspect} is not a valid tree node" if
104
+ node.is_a? Enumerable
105
+ parent = Path.__send__(:new, parent, node)
106
+ end
107
+
108
+ parent
109
+ end
110
+
111
+ alias + branch
112
+ alias / branch
113
+
114
+ ##
115
+ # @return [Path] the n-th last parent path
116
+ # @param distance [Integer] the number of nodes to go up
117
+ #
118
+ # @example
119
+ # path = Sycamore::Path[:foo, :bar, :baz]
120
+ # path.up # => Sycamore::Path[:foo, :bar]
121
+ # path.up(2) # => Sycamore::Path[:foo]
122
+ # path.up(3) # => Sycamore::Path[]
123
+ #
124
+ def up(distance = 1)
125
+ raise TypeError, "expected an integer, but got #{distance.inspect}" unless
126
+ distance.is_a? Integer
127
+
128
+ case distance
129
+ when 1 then @parent
130
+ when 0 then self
131
+ else parent.up(distance - 1)
132
+ end
133
+ end
134
+
135
+ ##
136
+ # @return [Boolean] if this is the root path
137
+ #
138
+ def root?
139
+ false
140
+ end
141
+
142
+ ##
143
+ # @return [Integer] the number of nodes on this path
144
+ #
145
+ def length
146
+ i, parent = 1, self
147
+ i += 1 until (parent = parent.parent).root?
148
+ i
149
+ end
150
+
151
+ alias size length
152
+
153
+ ##
154
+ # Iterates over all nodes on this path.
155
+ #
156
+ # @overload each_node
157
+ # @yield [node] each node
158
+ #
159
+ # @overload each_node
160
+ # @return [Enumerator<node>]
161
+ #
162
+ def each_node(&block)
163
+ return enum_for(__callee__) unless block_given?
164
+
165
+ if @parent
166
+ @parent.each_node(&block)
167
+ yield @node
168
+ end
169
+ end
170
+
171
+ alias each each_node
172
+
173
+ ##
174
+ # If a given structure contains this path.
175
+ #
176
+ # @param struct [Object]
177
+ # @return [Boolean] if the given structure contains the nodes on this path
178
+ #
179
+ # @example
180
+ # hash = {foo: {bar: :baz}}
181
+ # Sycamore::Path[:foo, :bar].present_in? hash # => true
182
+ # Sycamore::Path[:foo, :bar].present_in? Tree[hash] # => true
183
+ #
184
+ def present_in?(struct)
185
+ each do |node|
186
+ case
187
+ when struct.is_a?(Enumerable)
188
+ return false unless struct.include? node
189
+ struct = (Tree.like?(struct) ? struct[node] : Nothing )
190
+ else
191
+ return false unless struct.eql? node
192
+ struct = Nothing
193
+ end
194
+ end
195
+ true
196
+ end
197
+
198
+ alias in? present_in?
199
+
200
+ ########################################################################
201
+ # @group Equality
202
+ ########################################################################
203
+
204
+ ##
205
+ # @return [Fixnum] hash code for this path
206
+ #
207
+ def hash
208
+ to_a.hash ^ self.class.hash
209
+ end
210
+
211
+ ##
212
+ # @return [Boolean] if the other is a Path with the same nodes in the same order
213
+ # @param other [Object]
214
+ #
215
+ def eql?(other)
216
+ other.is_a?(self.class) and
217
+ self.length == other.length and begin
218
+ i = other.each ; all? { |node| node.eql? i.next }
219
+ end
220
+ end
221
+
222
+ ##
223
+ # @return [Boolean] if the other is an Enumerable with the same nodes in the same order
224
+ # @param other [Object]
225
+ #
226
+ def ==(other)
227
+ other.is_a?(Enumerable) and self.length == other.length and begin
228
+ i = other.each ; all? { |node| node == i.next }
229
+ end
230
+ end
231
+
232
+ ########################################################################
233
+ # @group Conversion
234
+ ########################################################################
235
+
236
+ ##
237
+ # @return [String] a string created by converting each node on this path to a string, separated by the given separator
238
+ # @param separator [String]
239
+ #
240
+ # @note Since the root path with no node is at the beginning of each path,
241
+ # the returned string always begins with the given separator.
242
+ #
243
+ # @example
244
+ # Sycamore::Path[1,2,3].join # => '/1/2/3'
245
+ # Sycamore::Path[1,2,3].join('|') # => '|1|2|3'
246
+ #
247
+ def join(separator = '/')
248
+ @parent.join(separator) + separator + node.to_s
249
+ end
250
+
251
+ ##
252
+ # @return [String] a compact string representation of this path
253
+ #
254
+ def to_s
255
+ "#<Path: #{join}>"
256
+ end
257
+
258
+ ##
259
+ # @return [String] a more verbose string representation of this path
260
+ #
261
+ def inspect
262
+ "#<Sycamore::Path[#{each_node.map(&:inspect).join(',')}]>"
263
+ end
264
+ end
265
+
266
+ end
@@ -0,0 +1,43 @@
1
+ require 'singleton'
2
+
3
+ module Sycamore
4
+ class Path
5
+ ##
6
+ # @api private
7
+ #
8
+ class Root < Path
9
+ include Singleton
10
+
11
+ def initialize
12
+ @parent, @node = nil, nil
13
+ end
14
+
15
+ def up(distance = 1)
16
+ super unless distance.is_a? Integer
17
+ self
18
+ end
19
+
20
+ def root?
21
+ true
22
+ end
23
+
24
+ def length
25
+ 0
26
+ end
27
+
28
+ def join(delimiter = '/')
29
+ ''
30
+ end
31
+
32
+ def to_s
33
+ '#<Path:Root>'
34
+ end
35
+
36
+ def inspect
37
+ '#<Sycamore::Path::Root>'
38
+ end
39
+ end
40
+
41
+ ROOT = Root.instance # @api private
42
+ end
43
+ end
@@ -0,0 +1,4 @@
1
+ require 'sycamore'
2
+
3
+ # optional global shortcut constant for Sycamore::Tree
4
+ STree = Sycamore::Tree