outstand-sycamore 0.4.0.pre
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.dockerignore +12 -0
- data/.editorconfig +24 -0
- data/.gitignore +12 -0
- data/.rspec +6 -0
- data/.travis.yml +16 -0
- data/.yardopts +13 -0
- data/AUTHORS +1 -0
- data/CHANGELOG.md +88 -0
- data/CONTRIBUTING.md +5 -0
- data/CREDITS +0 -0
- data/Deskfile +7 -0
- data/Dockerfile +78 -0
- data/Gemfile +12 -0
- data/Guardfile +11 -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/brew-shim +10 -0
- data/compose.yml +35 -0
- data/docker-entrypoint.sh +39 -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 +1470 -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 +154 -0
@@ -0,0 +1,39 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
set -euo pipefail
|
4
|
+
|
5
|
+
su-exec ${FIXUID:?Missing FIXUID var}:${FIXGID:?Missing FIXGID var} fixuid
|
6
|
+
|
7
|
+
chown_dir() {
|
8
|
+
dir=$1
|
9
|
+
if [[ -d ${dir} ]] && [[ "$(stat -c %u:%g ${dir})" != "${FIXUID}:${FIXGID}" ]]; then
|
10
|
+
echo chown $dir
|
11
|
+
chown sycamore:sycamore $dir
|
12
|
+
fi
|
13
|
+
}
|
14
|
+
|
15
|
+
chown_dir /usr/local/bundle
|
16
|
+
chown_dir /home/sycamore/.local/share/gem
|
17
|
+
chown_dir /home/sycamore/.gem
|
18
|
+
|
19
|
+
if [ "$(which "$1")" = '' ]; then
|
20
|
+
if [ "$(ls -A /usr/local/bundle/bin)" = '' ]; then
|
21
|
+
echo 'command not in path and bundler not initialized'
|
22
|
+
echo 'running bundle install'
|
23
|
+
su-exec sycamore bundle install
|
24
|
+
fi
|
25
|
+
fi
|
26
|
+
|
27
|
+
if [ "$1" = 'bundle' ]; then
|
28
|
+
set -- su-exec sycamore "$@"
|
29
|
+
elif [ "$1" = 'rake' ]; then
|
30
|
+
set -- su-exec sycamore bundle exec "$@"
|
31
|
+
|
32
|
+
su-exec sycamore bash -c 'bundle check || bundle install'
|
33
|
+
elif ls /usr/local/bundle/bin | grep -q "\b$1\b"; then
|
34
|
+
set -- su-exec sycamore bundle exec "$@"
|
35
|
+
|
36
|
+
su-exec sycamore bash -c 'bundle check || bundle install'
|
37
|
+
fi
|
38
|
+
|
39
|
+
exec "$@"
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'sycamore'
|
@@ -0,0 +1,179 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
|
3
|
+
module Sycamore
|
4
|
+
|
5
|
+
##
|
6
|
+
# An Absence object represents the absence of a specific child {Sycamore::Tree}.
|
7
|
+
#
|
8
|
+
# +Absence+ instances get created when accessing non-existent children of a
|
9
|
+
# Tree with {Tree#child_of} or {Tree#child_at}.
|
10
|
+
# It is not intended to be instantiated by the user.
|
11
|
+
#
|
12
|
+
# An +Absence+ object can be used like a normal {Sycamore::Tree}.
|
13
|
+
# {Tree::QUERY_METHODS Query} and {Tree::DESTRUCTIVE_COMMAND_METHODS pure destructive command method}
|
14
|
+
# calls get delegated to {Sycamore::Nothing}, i.e. will behave like an empty Tree.
|
15
|
+
# On every other {Tree::COMMAND_METHODS command} calls, the +Absence+ object
|
16
|
+
# gets resolved, which means the missing tree will be created, added to the
|
17
|
+
# parent tree and the method call gets delegated to the now existing tree.
|
18
|
+
# After the +Absence+ object is resolved all subsequent method calls are
|
19
|
+
# delegated to the created tree.
|
20
|
+
# The type of tree eventually created is determined by the {Tree#new_child}
|
21
|
+
# implementation of the parent tree and the parent node.
|
22
|
+
#
|
23
|
+
class Absence < Delegator
|
24
|
+
|
25
|
+
##
|
26
|
+
# @api private
|
27
|
+
#
|
28
|
+
def initialize(parent_tree, parent_node)
|
29
|
+
@parent_tree, @parent_node = parent_tree, parent_node
|
30
|
+
end
|
31
|
+
|
32
|
+
class << self
|
33
|
+
alias at new
|
34
|
+
end
|
35
|
+
|
36
|
+
########################################################################
|
37
|
+
# presence creation
|
38
|
+
########################################################################
|
39
|
+
|
40
|
+
##
|
41
|
+
# The tree object to which all method calls are delegated.
|
42
|
+
#
|
43
|
+
# @api private
|
44
|
+
#
|
45
|
+
def presence
|
46
|
+
@tree or Nothing
|
47
|
+
end
|
48
|
+
|
49
|
+
alias __getobj__ presence
|
50
|
+
|
51
|
+
##
|
52
|
+
# @api private
|
53
|
+
#
|
54
|
+
def create
|
55
|
+
@parent_tree = @parent_tree.add_node_with_empty_child(@parent_node)
|
56
|
+
@tree = @parent_tree[@parent_node]
|
57
|
+
end
|
58
|
+
|
59
|
+
########################################################################
|
60
|
+
# Absence and Nothing predicates
|
61
|
+
########################################################################
|
62
|
+
|
63
|
+
##
|
64
|
+
# (see Tree#absent?)
|
65
|
+
#
|
66
|
+
def absent?
|
67
|
+
@tree.nil?
|
68
|
+
end
|
69
|
+
|
70
|
+
##
|
71
|
+
# (see Tree#nothing?)
|
72
|
+
#
|
73
|
+
def nothing?
|
74
|
+
false
|
75
|
+
end
|
76
|
+
|
77
|
+
########################################################################
|
78
|
+
# Element access
|
79
|
+
########################################################################
|
80
|
+
|
81
|
+
#####################
|
82
|
+
# query methods #
|
83
|
+
#####################
|
84
|
+
|
85
|
+
def child_of(node)
|
86
|
+
if absent?
|
87
|
+
raise InvalidNode, "#{node} is not a valid tree node" if node.is_a? Enumerable
|
88
|
+
|
89
|
+
Absence.at(self, node)
|
90
|
+
else
|
91
|
+
presence.child_of(node)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def child_at(*path)
|
96
|
+
if absent?
|
97
|
+
# TODO: This is duplication of Tree#child_at! How can we remove it, without introducing a module for this single method or inherit from Tree?
|
98
|
+
case path.length
|
99
|
+
when 0 then raise ArgumentError, 'wrong number of arguments (given 0, expected 1+)'
|
100
|
+
when 1 then child_of(*path)
|
101
|
+
else child_of(path[0]).child_at(*path[1..-1])
|
102
|
+
end
|
103
|
+
else
|
104
|
+
presence.child_at(*path)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
alias [] child_at
|
109
|
+
alias dig child_at
|
110
|
+
|
111
|
+
##
|
112
|
+
# A developer-friendly string representation of the absent tree.
|
113
|
+
#
|
114
|
+
# @return [String]
|
115
|
+
#
|
116
|
+
def inspect
|
117
|
+
"#{absent? ? 'absent' : 'present'} child of node #{@parent_node.inspect} in #{@parent_tree.inspect}"
|
118
|
+
end
|
119
|
+
|
120
|
+
##
|
121
|
+
# Duplicates the resolved tree or raises an error, when unresolved.
|
122
|
+
#
|
123
|
+
# @return [Tree]
|
124
|
+
#
|
125
|
+
# @raise [TypeError] when this {Absence} is not resolved yet
|
126
|
+
#
|
127
|
+
def dup
|
128
|
+
presence.dup
|
129
|
+
end
|
130
|
+
|
131
|
+
##
|
132
|
+
# Clones the resolved tree or raises an error, when unresolved.
|
133
|
+
#
|
134
|
+
# @return [Tree]
|
135
|
+
#
|
136
|
+
# @raise [TypeError] when this {Absence} is not resolved yet
|
137
|
+
#
|
138
|
+
def clone
|
139
|
+
presence.clone
|
140
|
+
end
|
141
|
+
|
142
|
+
##
|
143
|
+
# Checks if the absent tree is frozen.
|
144
|
+
#
|
145
|
+
# @return [Boolean]
|
146
|
+
#
|
147
|
+
def frozen?
|
148
|
+
if absent?
|
149
|
+
false
|
150
|
+
else
|
151
|
+
presence.frozen?
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
#####################
|
156
|
+
# command methods #
|
157
|
+
#####################
|
158
|
+
|
159
|
+
# TODO: YARD should be informed about this method definitions.
|
160
|
+
Tree.command_methods.each do |command_method|
|
161
|
+
if Tree.pure_destructive_command_methods.include?(command_method)
|
162
|
+
define_method command_method do |*args|
|
163
|
+
if absent?
|
164
|
+
self
|
165
|
+
else
|
166
|
+
presence.send(command_method, *args) # TODO: How can we hand over a possible block? With eval etc.?
|
167
|
+
end
|
168
|
+
end
|
169
|
+
else
|
170
|
+
# TODO: This method should be atomic.
|
171
|
+
define_method command_method do |*args|
|
172
|
+
create if absent?
|
173
|
+
presence.send(command_method, *args) # TODO: How can we hand over a possible block? With eval etc.?
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
end
|
179
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Sycamore
|
2
|
+
# raised when a value is not a valid node
|
3
|
+
class InvalidNode < ArgumentError ; end
|
4
|
+
|
5
|
+
# raised when trying to call a additive command method of the {Nothing} tree
|
6
|
+
class NothingMutation < StandardError ; end
|
7
|
+
|
8
|
+
# raised when calling {Tree#node} or {Tree#node!} on a Tree with multiple nodes
|
9
|
+
class NonUniqueNodeSet < StandardError ; end
|
10
|
+
|
11
|
+
# raised when calling {Tree#node!} on a Tree without nodes
|
12
|
+
class EmptyNodeSet < StandardError ; end
|
13
|
+
|
14
|
+
# raised when trying to fetch the child of a leaf
|
15
|
+
class ChildError < KeyError ; end
|
16
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
|
3
|
+
module Sycamore
|
4
|
+
|
5
|
+
##
|
6
|
+
# The Nothing Tree singleton class.
|
7
|
+
#
|
8
|
+
# The Nothing Tree is an empty Sycamore Tree, and means "there are no nodes".
|
9
|
+
#
|
10
|
+
# It is immutable:
|
11
|
+
# - {Tree::QUERY_METHODS Query method} calls will behave like a normal, empty Tree.
|
12
|
+
# - {Tree::DESTRUCTIVE_COMMAND_METHODS Pure destructive command} calls, will be ignored, i.e. being no-ops.
|
13
|
+
# - But all other {Tree::COMMAND_METHODS command} calls, will raise a {NothingMutation}.
|
14
|
+
#
|
15
|
+
# It is the only Tree object that will return +true+ on a {#nothing?} call.
|
16
|
+
# But like {Absence}, it will return +true+ on {#absent?} and +false+ on {#existent?}.
|
17
|
+
#
|
18
|
+
class NothingTree < Tree
|
19
|
+
include Singleton
|
20
|
+
|
21
|
+
########################################################################
|
22
|
+
# Absence and Nothing predicates
|
23
|
+
########################################################################
|
24
|
+
|
25
|
+
##
|
26
|
+
# (see Tree#nothing?)
|
27
|
+
#
|
28
|
+
def nothing?
|
29
|
+
true
|
30
|
+
end
|
31
|
+
|
32
|
+
##
|
33
|
+
# (see Tree#absent?)
|
34
|
+
#
|
35
|
+
def absent?
|
36
|
+
true
|
37
|
+
end
|
38
|
+
|
39
|
+
########################################################################
|
40
|
+
# CQS element access
|
41
|
+
########################################################################
|
42
|
+
|
43
|
+
# TODO: YARD should be informed about this method definitions.
|
44
|
+
command_methods.each do |command_method|
|
45
|
+
define_method command_method do |*args|
|
46
|
+
raise NothingMutation, 'attempt to change the Nothing tree'
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# TODO: YARD should be informed about this method definitions.
|
51
|
+
pure_destructive_command_methods.each do |command_method|
|
52
|
+
define_method(command_method) { |*args| self }
|
53
|
+
end
|
54
|
+
|
55
|
+
def child_of(node)
|
56
|
+
self
|
57
|
+
end
|
58
|
+
|
59
|
+
def to_native_object(sleaf_child_as: nil, **args)
|
60
|
+
sleaf_child_as
|
61
|
+
end
|
62
|
+
|
63
|
+
##
|
64
|
+
# A string representation of the Nothing tree.
|
65
|
+
#
|
66
|
+
# @return [String]
|
67
|
+
#
|
68
|
+
def to_s
|
69
|
+
'Tree[Nothing]'
|
70
|
+
end
|
71
|
+
|
72
|
+
##
|
73
|
+
# A developer-friendly string representation of the Nothing tree.
|
74
|
+
#
|
75
|
+
# @return [String]
|
76
|
+
#
|
77
|
+
def inspect
|
78
|
+
'#<Sycamore::Nothing>'
|
79
|
+
end
|
80
|
+
|
81
|
+
def freeze
|
82
|
+
super
|
83
|
+
end
|
84
|
+
|
85
|
+
########################################################################
|
86
|
+
# Equality
|
87
|
+
########################################################################
|
88
|
+
|
89
|
+
##
|
90
|
+
# Checks if the given object is an empty tree.
|
91
|
+
#
|
92
|
+
# @return [Boolean]
|
93
|
+
#
|
94
|
+
def ==(other)
|
95
|
+
(other.is_a?(Tree) or other.is_a?(Absence)) and other.empty?
|
96
|
+
end
|
97
|
+
|
98
|
+
|
99
|
+
########################################################################
|
100
|
+
# Falsiness
|
101
|
+
#
|
102
|
+
# Sadly, in Ruby we can't do that match to reach this goal.
|
103
|
+
#
|
104
|
+
# see http://devblog.avdi.org/2011/05/30/null-objects-and-falsiness/
|
105
|
+
########################################################################
|
106
|
+
|
107
|
+
##
|
108
|
+
# Try to emulate a falsey value, by negating to +true+.
|
109
|
+
#
|
110
|
+
# @return [Boolean] +true+
|
111
|
+
#
|
112
|
+
# @see http://devblog.avdi.org/2011/05/30/null-objects-and-falsiness/
|
113
|
+
#
|
114
|
+
def !
|
115
|
+
true
|
116
|
+
end
|
117
|
+
|
118
|
+
# def nil?
|
119
|
+
# true
|
120
|
+
# end
|
121
|
+
|
122
|
+
|
123
|
+
########################################################################
|
124
|
+
# Some helpers
|
125
|
+
#
|
126
|
+
# Ideally these would be implemented with Refinements, but since they
|
127
|
+
# aren't available anywhere (I'm looking at you, JRuby), we have to be
|
128
|
+
# content with this.
|
129
|
+
#
|
130
|
+
########################################################################
|
131
|
+
|
132
|
+
def like?(object)
|
133
|
+
object.nil? or object.equal? self
|
134
|
+
end
|
135
|
+
|
136
|
+
##
|
137
|
+
# @api private
|
138
|
+
class NestedStringPresentation
|
139
|
+
include Singleton
|
140
|
+
|
141
|
+
def inspect
|
142
|
+
'n/a'
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
##
|
147
|
+
# @api private
|
148
|
+
NestedString = NestedStringPresentation.instance.freeze
|
149
|
+
|
150
|
+
end
|
151
|
+
|
152
|
+
############################################################################
|
153
|
+
# The Nothing Tree Singleton object
|
154
|
+
#
|
155
|
+
Nothing = NothingTree.instance.freeze
|
156
|
+
|
157
|
+
end
|
@@ -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
|