metaractor-sycamore 0.4.1

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