root 0.0.1

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