sycamore 0.1.0
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/.gitignore +11 -0
- data/.rspec +6 -0
- data/.ruby-version +1 -0
- data/.travis.yml +9 -0
- data/.yardopts +11 -0
- data/AUTHORS +1 -0
- data/CHANGELOG.md +10 -0
- data/CONTRIBUTING.md +5 -0
- data/CREDITS +0 -0
- data/Gemfile +7 -0
- data/Guardfile +11 -0
- data/LICENSE.txt +22 -0
- data/README.md +516 -0
- data/Rakefile +31 -0
- data/VERSION +1 -0
- data/bin/console +9 -0
- data/bin/setup +8 -0
- data/lib/sycamore.rb +13 -0
- data/lib/sycamore/absence.rb +179 -0
- data/lib/sycamore/exceptions.rb +10 -0
- data/lib/sycamore/extension.rb +2 -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/nothing.rb +157 -0
- data/lib/sycamore/path.rb +261 -0
- data/lib/sycamore/path_root.rb +43 -0
- data/lib/sycamore/stree.rb +4 -0
- data/lib/sycamore/tree.rb +1272 -0
- data/lib/sycamore/version.rb +28 -0
- data/script/console +14 -0
- data/script/console.cmd +1 -0
- data/support/doctest_helper.rb +2 -0
- data/sycamore.gemspec +28 -0
- metadata +178 -0
data/Rakefile
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
RSpec::Core::RakeTask.new(:spec)
|
6
|
+
rescue LoadError
|
7
|
+
puts "Couldn't find RSpec core Rake task"
|
8
|
+
end
|
9
|
+
|
10
|
+
begin
|
11
|
+
require 'yard'
|
12
|
+
YARD::Rake::YardocTask.new do |t|
|
13
|
+
t.options = ['--verbose']
|
14
|
+
t.files = ['lib/**/*.rb', 'doc/**/*.md']
|
15
|
+
t.stats_options = ['--list-undoc']
|
16
|
+
end
|
17
|
+
rescue LoadError
|
18
|
+
puts "Couldn't find YARD"
|
19
|
+
end
|
20
|
+
|
21
|
+
begin
|
22
|
+
require 'yard-doctest'
|
23
|
+
YARD::Doctest::RakeTask.new do |task|
|
24
|
+
task.doctest_opts = %w[]
|
25
|
+
task.pattern = 'lib/**/*.rb'
|
26
|
+
end
|
27
|
+
rescue LoadError
|
28
|
+
puts "Couldn't find yard-doctest"
|
29
|
+
end
|
30
|
+
|
31
|
+
task :default => [:spec, 'yard:doctest']
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/bin/console
ADDED
data/bin/setup
ADDED
data/lib/sycamore.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'sycamore/version'
|
2
|
+
require 'sycamore/exceptions'
|
3
|
+
require 'sycamore/path'
|
4
|
+
require 'sycamore/path_root'
|
5
|
+
require 'sycamore/tree'
|
6
|
+
require 'sycamore/nothing'
|
7
|
+
require 'sycamore/absence'
|
8
|
+
|
9
|
+
##
|
10
|
+
# see README.md
|
11
|
+
#
|
12
|
+
module Sycamore
|
13
|
+
end
|
@@ -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,10 @@
|
|
1
|
+
module Sycamore
|
2
|
+
# raised when a value is not a valid node
|
3
|
+
class InvalidNode < ArgumentError ; end
|
4
|
+
# raised when trying to call a additive command method of the {Nothing} tree
|
5
|
+
class NothingMutation < StandardError ; end
|
6
|
+
# raised when calling {Tree#node} on a Tree with multiple nodes
|
7
|
+
class NonUniqueNodeSet < StandardError ; end
|
8
|
+
# raised when trying to fetch the child of a leaf
|
9
|
+
class ChildError < KeyError ; end
|
10
|
+
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,261 @@
|
|
1
|
+
module Sycamore
|
2
|
+
|
3
|
+
##
|
4
|
+
# A compact, immutable representation of Tree paths, i.e. node sequences.
|
5
|
+
#
|
6
|
+
# This class is optimized for its usage in {Tree#each_path}, where it
|
7
|
+
# can efficiently represent the whole tree as a set of paths by sharing the
|
8
|
+
# parent paths.
|
9
|
+
# It is not intended to be instantiated by the user.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# tree = Tree[foo: [:bar, :baz]]
|
13
|
+
# path1, path2 = tree.paths.to_a
|
14
|
+
# path1 == Sycamore::Path[:foo, :bar] # => true
|
15
|
+
# path2 == Sycamore::Path[:foo, :baz] # => true
|
16
|
+
# path1.parent.equal? path2.parent # => true
|
17
|
+
#
|
18
|
+
# @todo Measure the performance and memory consumption in comparison with a
|
19
|
+
# pure Array-based implementation (where tree nodes are duplicated), esp. in
|
20
|
+
# the most common use case of property-value structures.
|
21
|
+
#
|
22
|
+
class Path
|
23
|
+
include Enumerable
|
24
|
+
|
25
|
+
attr_reader :node, :parent
|
26
|
+
|
27
|
+
########################################################################
|
28
|
+
# @group Construction
|
29
|
+
########################################################################
|
30
|
+
|
31
|
+
##
|
32
|
+
# @private
|
33
|
+
#
|
34
|
+
def initialize(parent, node)
|
35
|
+
@parent, @node = parent, node
|
36
|
+
end
|
37
|
+
|
38
|
+
##
|
39
|
+
# @return the root of all Paths
|
40
|
+
#
|
41
|
+
def self.root
|
42
|
+
ROOT
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# Creates a new path.
|
47
|
+
#
|
48
|
+
# Depending on whether the first argument is a {Path}, the new Path is
|
49
|
+
# {#branch}ed from this path or the {root}.
|
50
|
+
#
|
51
|
+
# @overload of(path, nodes)
|
52
|
+
# @param path [Path] the path from which should be {#branch}ed
|
53
|
+
# @param nodes [nodes]
|
54
|
+
# @return [Path] the {#branch}ed path from the given path, with the given nodes expanded
|
55
|
+
#
|
56
|
+
# @overload of(nodes)
|
57
|
+
# @param nodes [nodes]
|
58
|
+
# @return [Path] the {#branch}ed path from the {root}, with the given nodes
|
59
|
+
#
|
60
|
+
def self.of(*args)
|
61
|
+
if (parent = args.first).is_a? Path
|
62
|
+
parent.branch(*args[1..-1])
|
63
|
+
else
|
64
|
+
root.branch(*args)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class << self
|
69
|
+
private :new # disable Path.new
|
70
|
+
|
71
|
+
alias [] of
|
72
|
+
end
|
73
|
+
|
74
|
+
########################################################################
|
75
|
+
# @group Elements
|
76
|
+
########################################################################
|
77
|
+
|
78
|
+
##
|
79
|
+
# Returns a new path based on this path, but with the given nodes extended.
|
80
|
+
#
|
81
|
+
# @param nodes [nodes] an arbitrary number of nodes
|
82
|
+
# @return [Path]
|
83
|
+
#
|
84
|
+
# @raise [InvalidNode] if one or more of the given nodes is an Enumerable
|
85
|
+
#
|
86
|
+
# @example
|
87
|
+
# path = Sycamore::Path[:foo, :bar]
|
88
|
+
# path.branch(:baz, :qux) ==
|
89
|
+
# Sycamore::Path[:foo, :bar, :baz, :qux] # => true
|
90
|
+
# path / :baz / :qux ==
|
91
|
+
# Sycamore::Path[:foo, :bar, :baz, :qux] # => true
|
92
|
+
#
|
93
|
+
def branch(*nodes)
|
94
|
+
return branch(*nodes.first) if nodes.size == 1 and nodes.first.is_a? Enumerable
|
95
|
+
|
96
|
+
parent = self
|
97
|
+
nodes.each do |node|
|
98
|
+
raise InvalidNode, "#{node} in Path #{nodes.inspect} is not a valid tree node" if
|
99
|
+
node.is_a? Enumerable
|
100
|
+
parent = Path.__send__(:new, parent, node)
|
101
|
+
end
|
102
|
+
|
103
|
+
parent
|
104
|
+
end
|
105
|
+
|
106
|
+
alias + branch
|
107
|
+
alias / branch
|
108
|
+
|
109
|
+
##
|
110
|
+
# @return [Path] the n-th last parent path
|
111
|
+
# @param distance [Integer] the number of nodes to go up
|
112
|
+
#
|
113
|
+
# @example
|
114
|
+
# path = Sycamore::Path[:foo, :bar, :baz]
|
115
|
+
# path.up # => Sycamore::Path[:foo, :bar]
|
116
|
+
# path.up(2) # => Sycamore::Path[:foo]
|
117
|
+
# path.up(3) # => Sycamore::Path[]
|
118
|
+
#
|
119
|
+
def up(distance = 1)
|
120
|
+
raise TypeError, "expected an integer, but got #{distance.inspect}" unless
|
121
|
+
distance.is_a? Integer
|
122
|
+
|
123
|
+
case distance
|
124
|
+
when 1 then @parent
|
125
|
+
when 0 then self
|
126
|
+
else parent.up(distance - 1)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
##
|
131
|
+
# @return [Boolean] if this is the root path
|
132
|
+
#
|
133
|
+
def root?
|
134
|
+
false
|
135
|
+
end
|
136
|
+
|
137
|
+
##
|
138
|
+
# @return [Integer] the number of nodes on this path
|
139
|
+
#
|
140
|
+
def length
|
141
|
+
i, parent = 1, self
|
142
|
+
i += 1 until (parent = parent.parent).root?
|
143
|
+
i
|
144
|
+
end
|
145
|
+
|
146
|
+
alias size length
|
147
|
+
|
148
|
+
##
|
149
|
+
# Iterates over all nodes on this path.
|
150
|
+
#
|
151
|
+
# @overload each_node
|
152
|
+
# @yield [node] each node
|
153
|
+
#
|
154
|
+
# @overload each_node
|
155
|
+
# @return [Enumerator<node>]
|
156
|
+
#
|
157
|
+
def each_node(&block)
|
158
|
+
return enum_for(__callee__) unless block_given?
|
159
|
+
|
160
|
+
if @parent
|
161
|
+
@parent.each_node(&block)
|
162
|
+
yield @node
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
alias each each_node
|
167
|
+
|
168
|
+
##
|
169
|
+
# If a given structure contains this path.
|
170
|
+
#
|
171
|
+
# @param struct [Object]
|
172
|
+
# @return [Boolean] if the given structure contains the nodes on this path
|
173
|
+
#
|
174
|
+
# @example
|
175
|
+
# hash = {foo: {bar: :baz}}
|
176
|
+
# Sycamore::Path[:foo, :bar].present_in? hash # => true
|
177
|
+
# Sycamore::Path[:foo, :bar].present_in? Tree[hash] # => true
|
178
|
+
#
|
179
|
+
def present_in?(struct)
|
180
|
+
each do |node|
|
181
|
+
case
|
182
|
+
when struct.is_a?(Enumerable)
|
183
|
+
return false unless struct.include? node
|
184
|
+
struct = (Tree.like?(struct) ? struct[node] : Nothing )
|
185
|
+
else
|
186
|
+
return false unless struct.eql? node
|
187
|
+
struct = Nothing
|
188
|
+
end
|
189
|
+
end
|
190
|
+
true
|
191
|
+
end
|
192
|
+
|
193
|
+
alias in? present_in?
|
194
|
+
|
195
|
+
########################################################################
|
196
|
+
# @group Equality
|
197
|
+
########################################################################
|
198
|
+
|
199
|
+
##
|
200
|
+
# @return [Fixnum] hash code for this path
|
201
|
+
#
|
202
|
+
def hash
|
203
|
+
to_a.hash ^ self.class.hash
|
204
|
+
end
|
205
|
+
|
206
|
+
##
|
207
|
+
# @return [Boolean] if the other is a Path with the same nodes in the same order
|
208
|
+
# @param other [Object]
|
209
|
+
#
|
210
|
+
def eql?(other)
|
211
|
+
other.is_a?(self.class) and
|
212
|
+
self.length == other.length and begin
|
213
|
+
i = other.each ; all? { |node| node.eql? i.next }
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
##
|
218
|
+
# @return [Boolean] if the other is an Enumerable with the same nodes in the same order
|
219
|
+
# @param other [Object]
|
220
|
+
#
|
221
|
+
def ==(other)
|
222
|
+
other.is_a?(Enumerable) and self.length == other.length and begin
|
223
|
+
i = other.each ; all? { |node| node == i.next }
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
########################################################################
|
228
|
+
# @group Conversion
|
229
|
+
########################################################################
|
230
|
+
|
231
|
+
##
|
232
|
+
# @return [String] a string created by converting each node on this path to a string, separated by the given separator
|
233
|
+
# @param separator [String]
|
234
|
+
#
|
235
|
+
# @note Since the root path with no node is at the beginning of each path,
|
236
|
+
# the returned string always begins with the given separator.
|
237
|
+
#
|
238
|
+
# @example
|
239
|
+
# Sycamore::Path[1,2,3].join # => '/1/2/3'
|
240
|
+
# Sycamore::Path[1,2,3].join('|') # => '|1|2|3'
|
241
|
+
#
|
242
|
+
def join(separator = '/')
|
243
|
+
@parent.join(separator) + separator + node.to_s
|
244
|
+
end
|
245
|
+
|
246
|
+
##
|
247
|
+
# @return [String] a compact string representation of this path
|
248
|
+
#
|
249
|
+
def to_s
|
250
|
+
"#<Path: #{join}>"
|
251
|
+
end
|
252
|
+
|
253
|
+
##
|
254
|
+
# @return [String] a more verbose string representation of this path
|
255
|
+
#
|
256
|
+
def inspect
|
257
|
+
"#<Sycamore::Path[#{each_node.map(&:inspect).join(',')}]>"
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
end
|