sycamore 0.1.0
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/.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
|