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.
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
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'sycamore'
5
+
6
+ require 'pry'
7
+ Pry.start
8
+
9
+ include Sycamore # TODO: require 'sycamore/extension'
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
8
+
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,2 @@
1
+ require 'sycamore/extension/tree'
2
+ require 'sycamore/extension/nothing'
@@ -0,0 +1,4 @@
1
+ require 'sycamore/nothing'
2
+
3
+ # optional global shortcut constant for {Sycamore::Nothing}
4
+ Nothing = Sycamore::Nothing unless defined? Nothing
@@ -0,0 +1,7 @@
1
+ require 'sycamore'
2
+
3
+ # optional global shortcut constant for Sycamore::Path
4
+ Path = Sycamore::Path
5
+
6
+ # optional global shortcut constant for Sycamore::Path
7
+ TreePath = Sycamore::Path
@@ -0,0 +1,4 @@
1
+ require 'sycamore'
2
+
3
+ # optional global shortcut constant for Sycamore::Tree
4
+ Tree = Sycamore::Tree
@@ -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