metaractor-sycamore 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.editorconfig +24 -0
- data/.envrc +3 -0
- data/.github/workflows/specs.yml +13 -0
- data/.gitignore +16 -0
- data/.rspec +6 -0
- data/.ruby-version +1 -0
- data/.yardopts +13 -0
- data/AUTHORS +2 -0
- data/CHANGELOG.md +88 -0
- data/CONTRIBUTING.md +5 -0
- data/CREDITS +0 -0
- data/Gemfile +12 -0
- data/LICENSE.txt +22 -0
- data/README.md +571 -0
- data/Rakefile +36 -0
- data/VERSION +1 -0
- data/bin/console +7 -0
- data/bin/setup +8 -0
- data/devenv.lock +210 -0
- data/devenv.nix +25 -0
- data/devenv.yaml +8 -0
- data/lib/outstand_sycamore.rb +1 -0
- data/lib/sycamore/absence.rb +179 -0
- data/lib/sycamore/exceptions.rb +16 -0
- data/lib/sycamore/extension/nothing.rb +4 -0
- data/lib/sycamore/extension/path.rb +7 -0
- data/lib/sycamore/extension/tree.rb +4 -0
- data/lib/sycamore/extension.rb +2 -0
- data/lib/sycamore/nothing.rb +157 -0
- data/lib/sycamore/path.rb +266 -0
- data/lib/sycamore/path_root.rb +43 -0
- data/lib/sycamore/stree.rb +4 -0
- data/lib/sycamore/tree.rb +1469 -0
- data/lib/sycamore/version.rb +28 -0
- data/lib/sycamore.rb +13 -0
- data/support/doctest_helper.rb +2 -0
- data/support/travis.sh +6 -0
- data/sycamore.gemspec +29 -0
- metadata +152 -0
@@ -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
|