sycamore 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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