outstand-sycamore 0.4.0.pre

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