outstand-sycamore 0.4.0.pre

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.
@@ -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,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,2 @@
1
+ require 'sycamore/extension/tree'
2
+ require 'sycamore/extension/nothing'
@@ -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