root 0.0.1

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,57 @@
1
+ class Root
2
+
3
+ # StringExt provides two common string transformations, camelize and
4
+ # underscore. StringExt is automatically included in String.
5
+ #
6
+ # Both methods are directly taken from the ActiveSupport {Inflections}[http://api.rubyonrails.org/classes/ActiveSupport/CoreExtensions/String/Inflections.html]
7
+ # module. StringExt should not cause conflicts if ActiveSupport is
8
+ # loaded alongside Root.
9
+ #
10
+ # ActiveSupport is distributed with an MIT-LICENSE:
11
+ #
12
+ # Copyright (c) 2004-2008 David Heinemeier Hansson
13
+ #
14
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
15
+ # associated documentation files (the "Software"), to deal in the Software without restriction,
16
+ # including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
17
+ # and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
18
+ # subject to the following conditions:
19
+ #
20
+ # The above copyright notice and this permission notice shall be included in all copies or substantial
21
+ # portions of the Software.
22
+ #
23
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
24
+ # LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
25
+ # NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
26
+ # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
27
+ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
28
+ #
29
+ module StringExt
30
+
31
+ # camelize converts self to UpperCamelCase. If the argument to
32
+ # camelize is set to :lower then camelize produces lowerCamelCase.
33
+ # camelize will also convert '/' to '::' which is useful for
34
+ # converting paths to namespaces.
35
+ def camelize(first_letter = :upper)
36
+ case first_letter
37
+ when :upper then self.to_s.gsub(/\/(.?)/) { "::" + $1.upcase }.gsub(/(^|_)(.)/) { $2.upcase }
38
+ when :lower then self.first + camelize[1..-1]
39
+ end
40
+ end
41
+
42
+ # The reverse of camelize. Makes an underscored, lowercase form
43
+ # from self. underscore will also change '::' to '/' to convert
44
+ # namespaces to paths.
45
+ def underscore
46
+ self.gsub(/::/, '/').
47
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
48
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
49
+ tr("-", "_").
50
+ downcase
51
+ end
52
+ end
53
+ end
54
+
55
+ class String # :nodoc:
56
+ include Root::StringExt
57
+ end
@@ -0,0 +1,391 @@
1
+ require 'root/versions'
2
+ autoload(:FileUtils, 'fileutils')
3
+
4
+ class Root
5
+ module Utils
6
+ include Versions
7
+
8
+ # Regexp to match a windows-style root filepath.
9
+ WIN_ROOT_PATTERN = /^[A-z]:\//
10
+
11
+ module_function
12
+
13
+ # Returns the filepath of path relative to dir. Both dir and path are
14
+ # expanded before the relative filepath is determined. Returns nil if
15
+ # the path is not relative to dir.
16
+ #
17
+ # relative_filepath('dir', "dir/path/to/file.txt") # => "path/to/file.txt"
18
+ #
19
+ def relative_filepath(dir, path, dir_string=Dir.pwd)
20
+ expanded_dir = File.expand_path(dir, dir_string)
21
+ expanded_path = File.expand_path(path, dir_string)
22
+
23
+ return nil unless expanded_path.index(expanded_dir) == 0
24
+
25
+ # use dir.length + 1 to remove a leading '/'. If dir.length + 1 >= expanded.length
26
+ # as in: relative_filepath('/path', '/path') then the first arg returns nil, and an
27
+ # empty string is returned
28
+ expanded_path[(expanded_dir.chomp("/").length + 1)..-1] || ""
29
+ end
30
+
31
+ # Generates a target filepath translated from the source_dir to the
32
+ # target_dir. Raises an error if the filepath is not relative to the
33
+ # source_dir.
34
+ #
35
+ # translate("/path/to/file.txt", "/path", "/another/path") # => '/another/path/to/file.txt'
36
+ #
37
+ def translate(path, source_dir, target_dir)
38
+ unless relative_path = relative_filepath(source_dir, path)
39
+ raise ArgumentError, "\n#{path}\nis not relative to:\n#{source_dir}"
40
+ end
41
+ File.join(target_dir, relative_path)
42
+ end
43
+
44
+ # Returns the path, exchanging the extension with extname. Extname may
45
+ # optionally omit the leading period.
46
+ #
47
+ # exchange('path/to/file.txt', '.html') # => 'path/to/file.html'
48
+ # exchange('path/to/file.txt', 'rb') # => 'path/to/file.rb'
49
+ #
50
+ def exchange(path, extname)
51
+ "#{path.chomp(File.extname(path))}#{extname[0] == ?. ? '' : '.'}#{extname}"
52
+ end
53
+
54
+ # Lists all unique paths matching the input glob patterns.
55
+ def glob(*patterns)
56
+ patterns.collect do |pattern|
57
+ Dir.glob(pattern)
58
+ end.flatten.uniq
59
+ end
60
+
61
+ # Lists all unique versions of path matching the glob version patterns. If
62
+ # no patterns are specified, then all versions of path will be returned.
63
+ def vglob(path, *vpatterns)
64
+ vpatterns << "*" if vpatterns.empty?
65
+ vpatterns.collect do |vpattern|
66
+ results = Dir.glob(version(path, vpattern))
67
+
68
+ # extra work to include the default version path for any version
69
+ results << path if vpattern == "*" && File.exists?(path)
70
+ results
71
+ end.flatten.uniq
72
+ end
73
+
74
+ # Path suffix glob. Globs along the base paths for paths that match the
75
+ # specified suffix pattern.
76
+ def sglob(suffix_pattern, *base_paths)
77
+ base_paths.collect do |base|
78
+ base = File.expand_path(base)
79
+ Dir.glob(File.join(base, suffix_pattern))
80
+ end.flatten.uniq
81
+ end
82
+
83
+ # Like Dir.chdir but makes the directory, if necessary, when mkdir is
84
+ # specified. chdir raises an error for non-existant directories, as well
85
+ # as non-directory inputs.
86
+ def chdir(dir, mkdir=false, &block)
87
+ dir = File.expand_path(dir)
88
+
89
+ unless File.directory?(dir)
90
+ if !File.exists?(dir) && mkdir
91
+ FileUtils.mkdir_p(dir)
92
+ else
93
+ raise ArgumentError, "not a directory: #{dir}"
94
+ end
95
+ end
96
+
97
+ Dir.chdir(dir, &block)
98
+ end
99
+
100
+ # Prepares the input path by making the parent directory for path. If a
101
+ # block is given, a file is created at path and passed to it; in this
102
+ # way files with non-existant parent directories are readily made.
103
+ #
104
+ # Returns path.
105
+ def prepare(path, &block)
106
+ dirname = File.dirname(path)
107
+ FileUtils.mkdir_p(dirname) unless File.exists?(dirname)
108
+ File.open(path, "w", &block) if block_given?
109
+ path
110
+ end
111
+
112
+ # The path root type indicating windows, *nix, or some unknown style of
113
+ # filepaths (:win, :nix, :unknown).
114
+ def path_root_type
115
+ @path_root_type ||= case
116
+ when RUBY_PLATFORM =~ /mswin/ && File.expand_path(".") =~ WIN_ROOT_PATTERN then :win
117
+ when File.expand_path(".")[0] == ?/ then :nix
118
+ else :unknown
119
+ end
120
+ end
121
+
122
+ # Returns true if the input path appears to be an expanded path, based on
123
+ # path_root_type.
124
+ #
125
+ # If root_type == :win returns true if the path matches WIN_ROOT_PATTERN.
126
+ #
127
+ # expanded?('C:/path') # => true
128
+ # expanded?('c:/path') # => true
129
+ # expanded?('D:/path') # => true
130
+ # expanded?('path') # => false
131
+ #
132
+ # If root_type == :nix, then expanded? returns true if the path begins
133
+ # with '/'.
134
+ #
135
+ # expanded?('/path') # => true
136
+ # expanded?('path') # => false
137
+ #
138
+ # Otherwise expanded? always returns nil.
139
+ def expanded?(path, root_type=path_root_type)
140
+ case root_type
141
+ when :win
142
+ path =~ WIN_ROOT_PATTERN ? true : false
143
+ when :nix
144
+ path[0] == ?/
145
+ else
146
+ nil
147
+ end
148
+ end
149
+
150
+ # Trivial indicates when a path does not have content to load. Returns
151
+ # true if the file at path is empty, non-existant, a directory, or nil.
152
+ def trivial?(path)
153
+ path == nil || !File.file?(path) || File.size(path) == 0
154
+ end
155
+
156
+ # Empty returns true when dir is an existing directory that has no files.
157
+ def empty?(dir)
158
+ File.directory?(dir) && (Dir.entries(dir) - ['.', '..']).empty?
159
+ end
160
+
161
+ # Minimizes a set of paths to the set of shortest basepaths that unqiuely
162
+ # identify the paths. The path extension and versions are removed from
163
+ # the basepath if possible. For example:
164
+ #
165
+ # minimize ['path/to/a.rb', 'path/to/b.rb']
166
+ # # => ['a', 'b']
167
+ #
168
+ # minimize ['path/to/a-0.1.0.rb', 'path/to/b-0.1.0.rb']
169
+ # # => ['a', 'b']
170
+ #
171
+ # minimize ['path/to/file.rb', 'path/to/file.txt']
172
+ # # => ['file.rb', 'file.txt']
173
+ #
174
+ # minimize ['path-0.1/to/file.rb', 'path-0.2/to/file.rb']
175
+ # # => ['path-0.1/to/file', 'path-0.2/to/file']
176
+ #
177
+ # Minimized paths that carry their extension will always carry
178
+ # their version as well, but the converse is not true; paths
179
+ # can be minimized to carry just the version and not the path
180
+ # extension.
181
+ #
182
+ # minimize ['path/to/a-0.1.0.rb', 'path/to/a-0.1.0.txt']
183
+ # # => ['a-0.1.0.rb', 'a-0.1.0.txt']
184
+ #
185
+ # minimize ['path/to/a-0.1.0.rb', 'path/to/a-0.2.0.rb']
186
+ # # => ['a-0.1.0', 'a-0.2.0']
187
+ #
188
+ # If a block is given, each (path, mini-path) pair will be passed
189
+ # to it after minimization.
190
+ def minimize(paths) # :yields: path, mini_path
191
+ unless block_given?
192
+ mini_paths = []
193
+ minimize(paths) {|p, mp| mini_paths << mp }
194
+ return mini_paths
195
+ end
196
+
197
+ splits = paths.uniq.collect do |path|
198
+ extname = File.extname(path)
199
+ extname = '' if extname =~ /^\.\d+$/
200
+ base = File.basename(path.chomp(extname))
201
+ version = base =~ /(-\d+(\.\d+)*)$/ ? $1 : ''
202
+
203
+ [dirname_or_array(path), base.chomp(version), extname, version, false, path]
204
+ end
205
+
206
+ while !splits.empty?
207
+ index = 0
208
+ splits = splits.collect do |(dir, base, extname, version, flagged, path)|
209
+ index += 1
210
+ case
211
+ when !flagged && just_one?(splits, index, base)
212
+
213
+ # found just one
214
+ yield(path, base)
215
+ nil
216
+ when dir.kind_of?(Array)
217
+
218
+ # no more path segments to use, try to add
219
+ # back version and extname
220
+ if dir.empty?
221
+ dir << File.dirname(base)
222
+ base = File.basename(base)
223
+ end
224
+
225
+ case
226
+ when !version.empty?
227
+ # add back version (occurs first)
228
+ [dir, "#{base}#{version}", extname, '', false, path]
229
+
230
+ when !extname.empty?
231
+
232
+ # add back extension (occurs second)
233
+ [dir, "#{base}#{extname}", '', version, false, path]
234
+ else
235
+
236
+ # nothing more to distinguish... path is minimized (occurs third)
237
+ yield(path, min_join(dir[0], base))
238
+ nil
239
+ end
240
+ else
241
+
242
+ # shift path segment. dirname_or_array returns an
243
+ # array if this is the last path segment to shift.
244
+ [dirname_or_array(dir), min_join(File.basename(dir), base), extname, version, false, path]
245
+ end
246
+ end.compact
247
+ end
248
+ end
249
+
250
+ # Returns true if the mini_path matches path. Matching logic reverses
251
+ # that of minimize:
252
+ #
253
+ # * a match occurs when path ends with mini_path
254
+ # * if mini_path doesn't specify an extension, then mini_path
255
+ # must only match path up to the path extension
256
+ # * if mini_path doesn't specify a version, then mini_path
257
+ # must only match path up to the path basename (minus the
258
+ # version and extname)
259
+ #
260
+ # For example:
261
+ #
262
+ # minimal_match?('dir/file-0.1.0.rb', 'file') # => true
263
+ # minimal_match?('dir/file-0.1.0.rb', 'dir/file') # => true
264
+ # minimal_match?('dir/file-0.1.0.rb', 'file-0.1.0') # => true
265
+ # minimal_match?('dir/file-0.1.0.rb', 'file-0.1.0.rb') # => true
266
+ #
267
+ # minimal_match?('dir/file-0.1.0.rb', 'file.rb') # => false
268
+ # minimal_match?('dir/file-0.1.0.rb', 'file-0.2.0') # => false
269
+ # minimal_match?('dir/file-0.1.0.rb', 'another') # => false
270
+ #
271
+ # In matching, partial basenames are not allowed but partial directories
272
+ # are allowed. Hence:
273
+ #
274
+ # minimal_match?('dir/file-0.1.0.txt', 'file') # => true
275
+ # minimal_match?('dir/file-0.1.0.txt', 'ile') # => false
276
+ # minimal_match?('dir/file-0.1.0.txt', 'r/file') # => true
277
+ #
278
+ def minimal_match?(path, mini_path)
279
+ extname = non_version_extname(mini_path)
280
+ version = mini_path =~ /(-\d+(\.\d+)*)#{extname}$/ ? $1 : ''
281
+
282
+ match_path = case
283
+ when !extname.empty?
284
+ # force full match
285
+ path
286
+ when !version.empty?
287
+ # match up to version
288
+ path.chomp(non_version_extname(path))
289
+ else
290
+ # match up base
291
+ path.chomp(non_version_extname(path)).sub(/(-\d+(\.\d+)*)$/, '')
292
+ end
293
+
294
+ # key ends with pattern AND basenames of each are equal...
295
+ # the last check ensures that a full path segment has
296
+ # been specified
297
+ match_path[-mini_path.length, mini_path.length] == mini_path && File.basename(match_path) == File.basename(mini_path)
298
+ end
299
+
300
+ # Returns the path segments for the given path, splitting along the path
301
+ # divider. Root paths are always represented by a string, if only an
302
+ # empty string.
303
+ #
304
+ # os divider example
305
+ # windows '\' split('C:\path\to\file') # => ["C:", "path", "to", "file"]
306
+ # *nix '/' split('/path/to/file') # => ["", "path", "to", "file"]
307
+ #
308
+ # The path is always expanded relative to the expand_dir; so '.' and
309
+ # '..' are resolved. However, unless expand_path == true, only the
310
+ # segments relative to the expand_dir are returned.
311
+ #
312
+ # On windows (note that expanding paths allows the use of slashes or
313
+ # backslashes):
314
+ #
315
+ # Dir.pwd # => 'C:/'
316
+ # split('path\to\..\.\to\file') # => ["C:", "path", "to", "file"]
317
+ # split('path/to/.././to/file', false) # => ["path", "to", "file"]
318
+ #
319
+ # On *nix (or more generally systems with '/' roots):
320
+ #
321
+ # Dir.pwd # => '/'
322
+ # split('path/to/.././to/file') # => ["", "path", "to", "file"]
323
+ # split('path/to/.././to/file', false) # => ["path", "to", "file"]
324
+ #
325
+ def split(path, expand_path=true, expand_dir=Dir.pwd)
326
+ path = if expand_path
327
+ File.expand_path(path, expand_dir)
328
+ else
329
+ # normalize the path by expanding it, then
330
+ # work back to the relative filepath as needed
331
+ expanded_dir = File.expand_path(expand_dir)
332
+ expanded_path = File.expand_path(path, expand_dir)
333
+ expanded_path.index(expanded_dir) != 0 ? expanded_path : relative_filepath(expanded_dir, expanded_path)
334
+ end
335
+
336
+ segments = path.scan(/[^\/]+/)
337
+
338
+ # add back the root filepath as needed on *nix
339
+ segments.unshift "" if path[0] == ?/
340
+ segments
341
+ end
342
+
343
+ # utility method for minimize -- joins the
344
+ # dir and path, preventing results like:
345
+ #
346
+ # "./path"
347
+ # "//path"
348
+ #
349
+ def min_join(dir, path) # :nodoc:
350
+ case dir
351
+ when "." then path
352
+ when "/" then "/#{path}"
353
+ else "#{dir}/#{path}"
354
+ end
355
+ end
356
+
357
+ # utility method for minimize -- returns the
358
+ # dirname of path, or an array if the dirname
359
+ # is effectively empty.
360
+ def dirname_or_array(path) # :nodoc:
361
+ dir = File.dirname(path)
362
+ case dir
363
+ when path, '.' then []
364
+ else dir
365
+ end
366
+ end
367
+
368
+ # utility method for minimize -- determines if there
369
+ # is just one of the base in splits, while flagging
370
+ # all matching entries.
371
+ def just_one?(splits, index, base) # :nodoc:
372
+ just_one = true
373
+ index.upto(splits.length-1) do |i|
374
+ if splits[i][1] == base
375
+ splits[i][4] = true
376
+ just_one = false
377
+ end
378
+ end
379
+
380
+ just_one
381
+ end
382
+
383
+ # utility method for minimal_match -- returns a non-version
384
+ # extname, or an empty string if the path ends in a version.
385
+ def non_version_extname(path) # :nodoc:
386
+ extname = File.extname(path)
387
+ extname =~ /^\.\d+$/ ? '' : extname
388
+ end
389
+
390
+ end
391
+ end
@@ -0,0 +1,134 @@
1
+ class Root
2
+
3
+ # Version provides methods for adding, removing, and incrementing versions
4
+ # at the end of filepaths. Versions are all formatted like:
5
+ # 'filepath-version.extension'.
6
+ #
7
+ module Versions
8
+
9
+ # Adds a version to the filepath. Versioned filepaths follow the format:
10
+ # 'path-version.extension'. If no version is specified, then the
11
+ # filepath is returned.
12
+ #
13
+ # version("path/to/file.txt", 1.0) # => "path/to/file-1.0.txt"
14
+ #
15
+ def version(path, version)
16
+ version = version.to_s.strip
17
+ if version.empty?
18
+ path
19
+ else
20
+ extname = File.extname(path)
21
+ path.chomp(extname) + '-' + version + extname
22
+ end
23
+ end
24
+
25
+ # Increments the version of the filepath by the specified increment.
26
+ #
27
+ # increment("path/to/file-1.0.txt", "0.0.1") # => "path/to/file-1.0.1.txt"
28
+ # increment("path/to/file.txt", 1.0) # => "path/to/file-1.0.txt"
29
+ #
30
+ def increment(path, increment)
31
+ path, version = deversion(path)
32
+
33
+ # split the version and increment into integer arrays of equal length
34
+ increment, version = [increment, version].collect do |vstr|
35
+ begin
36
+ vstr.to_s.split(/\./).collect {|v| v.to_i}
37
+ rescue
38
+ raise "Bad version or increment: #{vstr}"
39
+ end
40
+ end
41
+ version.concat Array.new(increment.length - version.length, 0) if increment.length > version.length
42
+
43
+ # add the increment to version
44
+ 0.upto(version.length-1) do |i|
45
+ version[i] += (increment[i] || 0)
46
+ end
47
+
48
+ self.version(path, version.join("."))
49
+ end
50
+
51
+ # Splits the version from the input path, then returns the path and
52
+ # version. If no version is specified, then the returned version will be
53
+ # nil.
54
+ #
55
+ # deversion("path/to/file-1.0.txt") # => ["path/to/file.txt", "1.0"]
56
+ # deversion("path/to/file.txt") # => ["path/to/file.txt", nil]
57
+ #
58
+ def deversion(path)
59
+ extname = File.extname(path)
60
+ extname = '' if extname =~ /^\.\d+$/
61
+ path =~ /^(.*)-(\d(\.?\d)*)#{extname}$/ ? [$1 + extname, $2] : [path, nil]
62
+ end
63
+
64
+ # A <=> comparison for versions. compare_versions can take strings,
65
+ # integers, or even arrays representing the parts of a version.
66
+ #
67
+ # compare_versions("1.0.0", "0.9.9") # => 1
68
+ # compare_versions(1.1, 1.1) # => 0
69
+ # compare_versions([0,9], [0,9,1]) # => -1
70
+ def compare_versions(a,b)
71
+ a, b = [a,b].collect {|item| to_integer_array(item) }
72
+
73
+ # equalize the lengths of the integer arrays
74
+ d = b.length - a.length
75
+ case
76
+ when d < 0 then b.concat Array.new(-d, 0)
77
+ when d > 0 then a.concat Array.new(d, 0)
78
+ end
79
+
80
+ a <=> b
81
+ end
82
+
83
+ # Version unique. Select the latest or earliest versions of each file
84
+ # in the array. For paths that have no version, vniq considers any
85
+ # version to beat no version. The order of paths is preserved by
86
+ # default, but the extra sort doing so may be turned off.
87
+ #
88
+ # paths = [
89
+ # "/path/to/two-0.0.1.txt",
90
+ # "/path/to/one-0.0.1.txt",
91
+ # "/path/to/one.txt",
92
+ # "/path/to/two-1.0.1.txt",
93
+ # "/path/to/three.txt"]
94
+ #
95
+ # vniq(paths)
96
+ # # => [
97
+ # # "/path/to/one-0.0.1.txt",
98
+ # # "/path/to/two-1.0.1.txt",
99
+ # # "/path/to/three.txt"]
100
+ #
101
+ def vniq(array, earliest=false, preserve_order=true)
102
+ unique = {}
103
+ array.sort.each do |path|
104
+ base, version = deversion(path)
105
+ (unique[base] ||= []) << version
106
+ end
107
+
108
+ results = []
109
+ unique.each_pair do |base, versions|
110
+ versions = versions.sort {|a, b| compare_versions(a,b) }
111
+ winner = earliest ? versions.shift : versions.pop
112
+ results << version(base, winner)
113
+ end
114
+
115
+ results = results.sort_by do |path|
116
+ array.index(path)
117
+ end if preserve_order
118
+
119
+ results
120
+ end
121
+
122
+ private
123
+
124
+ # Converts an input argument (typically a string or an array) to an
125
+ # array of integers. Splits version string on "."
126
+ def to_integer_array(arg)
127
+ arr = case arg
128
+ when Array then arg
129
+ else arg.to_s.split('.')
130
+ end
131
+ arr.collect {|i| i.to_i}
132
+ end
133
+ end
134
+ end