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