tap 0.12.4 → 0.17.0

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.
Files changed (102) hide show
  1. data/History +34 -0
  2. data/README +62 -41
  3. data/bin/tap +36 -40
  4. data/cmd/console.rb +14 -6
  5. data/cmd/manifest.rb +62 -58
  6. data/cmd/run.rb +49 -31
  7. data/doc/API +84 -0
  8. data/doc/Class Reference +83 -115
  9. data/doc/Examples/Command Line +36 -0
  10. data/doc/Examples/Workflow +40 -0
  11. data/lib/tap/app.rb +293 -214
  12. data/lib/tap/app/node.rb +43 -0
  13. data/lib/tap/app/queue.rb +77 -0
  14. data/lib/tap/app/stack.rb +16 -0
  15. data/lib/tap/app/state.rb +22 -0
  16. data/lib/tap/constants.rb +2 -2
  17. data/lib/tap/env.rb +400 -314
  18. data/lib/tap/env/constant.rb +227 -0
  19. data/lib/tap/env/gems.rb +63 -0
  20. data/lib/tap/env/manifest.rb +89 -0
  21. data/lib/tap/env/minimap.rb +292 -0
  22. data/lib/tap/{support → env}/string_ext.rb +2 -2
  23. data/lib/tap/exe.rb +113 -125
  24. data/lib/tap/join.rb +175 -0
  25. data/lib/tap/joins.rb +9 -0
  26. data/lib/tap/joins/switch.rb +44 -0
  27. data/lib/tap/joins/sync.rb +99 -0
  28. data/lib/tap/root.rb +100 -491
  29. data/lib/tap/root/utils.rb +220 -0
  30. data/lib/tap/{support → root}/versions.rb +31 -29
  31. data/lib/tap/schema.rb +248 -0
  32. data/lib/tap/schema/parser.rb +413 -0
  33. data/lib/tap/schema/utils.rb +82 -0
  34. data/lib/tap/support/intern.rb +19 -6
  35. data/lib/tap/support/templater.rb +8 -3
  36. data/lib/tap/task.rb +175 -171
  37. data/lib/tap/tasks/dump.rb +58 -0
  38. data/lib/tap/tasks/load.rb +62 -0
  39. metadata +30 -73
  40. data/cmd/destroy.rb +0 -27
  41. data/cmd/generate.rb +0 -27
  42. data/doc/Command Reference +0 -105
  43. data/doc/Syntax Reference +0 -234
  44. data/doc/Tutorial +0 -348
  45. data/lib/tap/dump.rb +0 -142
  46. data/lib/tap/file_task.rb +0 -384
  47. data/lib/tap/generator/arguments.rb +0 -13
  48. data/lib/tap/generator/base.rb +0 -176
  49. data/lib/tap/generator/destroy.rb +0 -60
  50. data/lib/tap/generator/generate.rb +0 -93
  51. data/lib/tap/generator/generators/command/command_generator.rb +0 -21
  52. data/lib/tap/generator/generators/command/templates/command.erb +0 -32
  53. data/lib/tap/generator/generators/config/config_generator.rb +0 -98
  54. data/lib/tap/generator/generators/generator/generator_generator.rb +0 -37
  55. data/lib/tap/generator/generators/generator/templates/task.erb +0 -27
  56. data/lib/tap/generator/generators/generator/templates/test.erb +0 -26
  57. data/lib/tap/generator/generators/root/root_generator.rb +0 -84
  58. data/lib/tap/generator/generators/root/templates/MIT-LICENSE +0 -22
  59. data/lib/tap/generator/generators/root/templates/README +0 -14
  60. data/lib/tap/generator/generators/root/templates/Rakefile +0 -84
  61. data/lib/tap/generator/generators/root/templates/Rapfile +0 -11
  62. data/lib/tap/generator/generators/root/templates/gemspec +0 -27
  63. data/lib/tap/generator/generators/root/templates/test/tap_test_helper.rb +0 -3
  64. data/lib/tap/generator/generators/task/task_generator.rb +0 -25
  65. data/lib/tap/generator/generators/task/templates/task.erb +0 -14
  66. data/lib/tap/generator/generators/task/templates/test.erb +0 -19
  67. data/lib/tap/generator/manifest.rb +0 -20
  68. data/lib/tap/generator/preview.rb +0 -69
  69. data/lib/tap/load.rb +0 -64
  70. data/lib/tap/spec.rb +0 -41
  71. data/lib/tap/support/aggregator.rb +0 -65
  72. data/lib/tap/support/audit.rb +0 -333
  73. data/lib/tap/support/constant.rb +0 -143
  74. data/lib/tap/support/constant_manifest.rb +0 -126
  75. data/lib/tap/support/dependencies.rb +0 -54
  76. data/lib/tap/support/dependency.rb +0 -44
  77. data/lib/tap/support/executable.rb +0 -198
  78. data/lib/tap/support/executable_queue.rb +0 -125
  79. data/lib/tap/support/gems.rb +0 -43
  80. data/lib/tap/support/join.rb +0 -144
  81. data/lib/tap/support/joins.rb +0 -12
  82. data/lib/tap/support/joins/switch.rb +0 -27
  83. data/lib/tap/support/joins/sync_merge.rb +0 -38
  84. data/lib/tap/support/manifest.rb +0 -171
  85. data/lib/tap/support/minimap.rb +0 -90
  86. data/lib/tap/support/node.rb +0 -176
  87. data/lib/tap/support/parser.rb +0 -450
  88. data/lib/tap/support/schema.rb +0 -385
  89. data/lib/tap/support/shell_utils.rb +0 -67
  90. data/lib/tap/test.rb +0 -77
  91. data/lib/tap/test/assertions.rb +0 -38
  92. data/lib/tap/test/env_vars.rb +0 -29
  93. data/lib/tap/test/extensions.rb +0 -73
  94. data/lib/tap/test/file_test.rb +0 -362
  95. data/lib/tap/test/file_test_class.rb +0 -15
  96. data/lib/tap/test/regexp_escape.rb +0 -87
  97. data/lib/tap/test/script_test.rb +0 -46
  98. data/lib/tap/test/script_tester.rb +0 -115
  99. data/lib/tap/test/subset_test.rb +0 -260
  100. data/lib/tap/test/subset_test_class.rb +0 -99
  101. data/lib/tap/test/tap_test.rb +0 -109
  102. data/lib/tap/test/utils.rb +0 -231
@@ -0,0 +1,227 @@
1
+ require 'tap/env/string_ext'
2
+
3
+ module Tap
4
+ class Env
5
+
6
+ # A Constant serves as a placeholder for an actual constant, sort of like
7
+ # autoload. Use the constantize method to retrieve the actual constant; if
8
+ # it doesn't exist, constantize requires require_path and tries again.
9
+ #
10
+ # Object.const_defined?(:Net) # => false
11
+ # $".include?('net/http') # => false
12
+ #
13
+ # http = Constant.new('Net::HTTP', 'net/http.rb')
14
+ # http.constantize # => Net::HTTP
15
+ # $".include?('net/http.rb') # => true
16
+ #
17
+ # === Unloading
18
+ #
19
+ # Constant also supports constant unloading. Unloading can be useful in
20
+ # various development modes, but make cause code to behave unpredictably.
21
+ # When a Constant unloads, the constant value is detached from the nesting
22
+ # constant and the require path is removed from $". This allows a require
23
+ # statement to re-require, and in theory, reload the constant.
24
+ #
25
+ # # [simple.rb]
26
+ # # class Simple
27
+ # # end
28
+ #
29
+ # const = Constant.new('Simple', 'simple')
30
+ # const.constantize # => Simple
31
+ # Object.const_defined?(:Simple) # => true
32
+ #
33
+ # const.unload # => Simple
34
+ # Object.const_defined?(:Simple) # => false
35
+ #
36
+ # const.constantize # => Simple
37
+ # Object.const_defined?(:Simple) # => true
38
+ #
39
+ # Unloading and reloading works best for scripts that have no side effects;
40
+ # ie scripts that do not require other files and only define the specified
41
+ # class or module.
42
+ class Constant
43
+ class << self
44
+
45
+ # Constantize tries to look up the specified constant under const. A
46
+ # block may be given to manually look up missing constants; the last
47
+ # existing const and any non-existant constant names are yielded to the
48
+ # block, which is expected to return the desired constant. For instance
49
+ # in the example 'Non::Existant' is essentially mapping to ConstName.
50
+ #
51
+ # module ConstName; end
52
+ #
53
+ # Constant.constantize('ConstName') # => ConstName
54
+ # Constant.constantize('Non::Existant') { ConstName } # => ConstName
55
+ #
56
+ # Raises a NameError for invalid/missing constants.
57
+ def constantize(const_name, const=Object) # :yields: const, missing_const_names
58
+ unless CONST_REGEXP =~ const_name
59
+ raise NameError, "#{const_name.inspect} is not a valid constant name!"
60
+ end
61
+
62
+ constants = $1.split(/::/)
63
+ while !constants.empty?
64
+ unless const_is_defined?(const, constants[0])
65
+ if block_given?
66
+ return yield(const, constants)
67
+ else
68
+ raise NameError.new("uninitialized constant #{const_name}", constants[0])
69
+ end
70
+ end
71
+ const = const.const_get(constants.shift)
72
+ end
73
+ const
74
+ end
75
+
76
+ private
77
+
78
+ # helper method. Determines if the named constant is defined in const.
79
+ # The implementation (annoyingly) has to be different for ruby 1.9 due
80
+ # to changes in the API.
81
+ case RUBY_VERSION
82
+ when /^1.9/
83
+ def const_is_defined?(const, const_name) # :nodoc:
84
+ const.const_defined?(const_name, false)
85
+ end
86
+ else
87
+ def const_is_defined?(const, const_name) # :nodoc:
88
+ const.const_defined?(const_name)
89
+ end
90
+ end
91
+ end
92
+
93
+ # Matches a valid constant
94
+ CONST_REGEXP = /\A(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)\z/
95
+
96
+ # The full constant name
97
+ attr_reader :const_name
98
+
99
+ # The path to load to initialize a missing constant
100
+ attr_reader :require_path
101
+
102
+ # An optional comment
103
+ attr_accessor :comment
104
+
105
+ # Initializes a new Constant with the specified constant name,
106
+ # require_path, and comment. The const_name should be a valid
107
+ # constant name.
108
+ def initialize(const_name, require_path=nil, comment=nil)
109
+ @const_name = const_name
110
+ @require_path = require_path
111
+ @comment = comment
112
+ end
113
+
114
+ # Returns the underscored const_name.
115
+ #
116
+ # Constant.new("Const::Name").path # => 'const/name'
117
+ #
118
+ def path
119
+ @path ||= const_name.underscore
120
+ end
121
+
122
+ # Returns the basename of path.
123
+ #
124
+ # Constant.new("Const::Name").basename # => 'name'
125
+ #
126
+ def basename
127
+ @basename ||= File.basename(path)
128
+ end
129
+
130
+ # Returns the path, minus the basename of path.
131
+ #
132
+ # Constant.new("Const::Name").dirname # => 'const'
133
+ #
134
+ def dirname
135
+ @dirname ||= (dirname = File.dirname(path)) == "." ? "" : dirname
136
+ end
137
+
138
+ # Returns the name of the constant, minus nesting.
139
+ #
140
+ # Constant.new("Const::Name").name # => 'Name'
141
+ #
142
+ def name
143
+ @name ||= (const_name =~ /.*::(.*)\z/ ? $1 : const_name)
144
+ end
145
+
146
+ # Returns the nesting constant of const_name.
147
+ #
148
+ # Constant.new("Const::Name").nesting # => 'Const'
149
+ #
150
+ def nesting
151
+ @nesting ||= (const_name =~ /(.*)::.*\z/ ? $1 : '')
152
+ end
153
+
154
+ # Returns the number of constants in nesting.
155
+ #
156
+ # Constant.new("Const::Name").nesting_depth # => 1
157
+ #
158
+ def nesting_depth
159
+ @nesting_depth ||= nesting.split(/::/).length
160
+ end
161
+
162
+ # Returns the Lazydoc document for require_path.
163
+ def document
164
+ require_path ? Lazydoc[require_path] : nil
165
+ end
166
+
167
+ # True if another is a Constant with the same const_name,
168
+ # require_path, and comment as self.
169
+ def ==(another)
170
+ another.kind_of?(Constant) &&
171
+ another.const_name == self.const_name &&
172
+ another.require_path == self.require_path &&
173
+ another.comment == self.comment
174
+ end
175
+
176
+ # Looks up and returns the constant indicated by const_name. If the
177
+ # constant cannot be found, constantize requires require_path and
178
+ # tries again.
179
+ #
180
+ # Raises a NameError if the constant cannot be found.
181
+ def constantize
182
+ Constant.constantize(const_name) do
183
+ require require_path if require_path
184
+ Constant.constantize(const_name)
185
+ end
186
+ end
187
+
188
+ # Undefines the constant indicated by const_name. The nesting constants
189
+ # are not removed. If specified, require_path will be removed from $".
190
+ #
191
+ # When removing require_path, unload will add '.rb' to the require_path if
192
+ # require_path has no extension (this echos the behavior of require).
193
+ # Other extension names like '.so', '.dll', etc. are not tried and will
194
+ # not be removed.
195
+ #
196
+ # Does nothing if const_name doesn't exist. Returns the unloaded constant.
197
+ # Obviously, <em>this method should be used with caution</em>.
198
+ def unload(unrequire=true)
199
+ const = nesting.empty? ? Object : Constant.constantize(nesting) { Object }
200
+
201
+ if const.const_defined?(name)
202
+ if unrequire && require_path
203
+ path = File.extname(require_path).empty? ? "#{require_path}.rb" : require_path
204
+ $".delete(path)
205
+ end
206
+
207
+ return const.send(:remove_const, name)
208
+ end
209
+
210
+ nil
211
+ end
212
+
213
+ # Returns a string like:
214
+ #
215
+ # "#<Tap::Env::Constant:object_id Const::Name (require_path)>"
216
+ #
217
+ def inspect
218
+ "#<#{self.class}:#{object_id} #{const_name}#{@require_path == nil ? "" : " (#{@require_path})"}>"
219
+ end
220
+
221
+ # Returns the minikey for self, ie path. (see Tap::Env::Minimap)
222
+ def minikey
223
+ path
224
+ end
225
+ end
226
+ end
227
+ end
@@ -0,0 +1,63 @@
1
+ require 'rubygems'
2
+
3
+ module Tap
4
+ class Env
5
+
6
+ # Methods for working with {RubyGems}[http://www.rubygems.org/].
7
+ module Gems
8
+ module_function
9
+
10
+ # Returns the gemspec for the specified gem. A gem version
11
+ # can be specified in the name, like 'gem >= 1.2'. The gem
12
+ # is not activated by this method.
13
+ def gemspec(gem_name)
14
+ return gem_name if gem_name.kind_of?(Gem::Specification)
15
+
16
+ dependency = if gem_name.kind_of?(Gem::Dependency)
17
+ gem_name
18
+ else
19
+ # figure the version of the gem, by default >= 0.0.0
20
+ gem_name.to_s =~ /^([^~<=>]*)(.*)$/
21
+ name, version = $1.strip, $2
22
+ return nil if name.empty?
23
+ version = Gem::Requirement.default if version.empty?
24
+
25
+ # note the last gem matching the dependency requirements
26
+ # is the latest matching gem
27
+ Gem::Dependency.new(name, version)
28
+ end
29
+
30
+ Gem.source_index.search(dependency).last
31
+ end
32
+
33
+ # Selects gem specs for which the block returns true. If
34
+ # latest is specified, only the latest version of each
35
+ # gem will be passed to the block.
36
+ def select_gems(latest=true)
37
+ specs = latest ?
38
+ Gem.source_index.latest_specs :
39
+ Gem.source_index.gems.collect {|(name, spec)| spec }
40
+
41
+ # this song and dance is to ensure that specs are sorted
42
+ # by name (ascending) then version (descending) so that
43
+ # the latest version of a spec appears first
44
+ specs_by_name = {}
45
+ specs.each do |spec|
46
+ next unless !block_given? || yield(spec)
47
+ (specs_by_name[spec.name] ||= []) << spec
48
+ end
49
+
50
+ specs = []
51
+ specs_by_name.keys.sort.each do |name|
52
+ specs_by_name[name].sort_by do |spec|
53
+ spec.version
54
+ end.reverse_each do |spec|
55
+ specs << spec
56
+ end
57
+ end
58
+
59
+ specs
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,89 @@
1
+ require 'tap/env/minimap'
2
+ require 'tap/env/constant'
3
+
4
+ module Tap
5
+ class Env
6
+
7
+ class Manifest
8
+ include Enumerable
9
+ include Minimap
10
+
11
+ # The environment this manifest summarizes
12
+ attr_reader :env
13
+
14
+ attr_reader :type
15
+
16
+ # Initializes a new Manifest.
17
+ def initialize(env, type)
18
+ @env = env
19
+ @type = type
20
+ end
21
+
22
+ def entries
23
+ env.registry[type]
24
+ end
25
+
26
+ # True if entries are empty.
27
+ def empty?
28
+ entries.empty?
29
+ end
30
+
31
+ def all_empty?
32
+ env.all? do |current|
33
+ current.manifest(type).empty?
34
+ end
35
+ end
36
+
37
+ # Iterates over each entry in self.
38
+ def each
39
+ entries.each {|entry| yield(entry) }
40
+ end
41
+
42
+ # Searches across env.each for the first entry minimatching key. A single
43
+ # env can be specified by using a compound key like 'env_key:key'.
44
+ #
45
+ # Returns nil if no matching entry is found.
46
+ def seek(key)
47
+ env.seek(type, key)
48
+ end
49
+
50
+ def [](key)
51
+ entry = seek(key)
52
+ entry.kind_of?(Constant) ? entry.constantize : entry
53
+ end
54
+
55
+ # Same as env.inspect but adds manifest to the templater
56
+ def inspect(template=nil, globals={}, filename=nil)
57
+ return super() unless template
58
+
59
+ env.inspect(template, globals, filename) do |templater, globalz|
60
+ env = templater.env
61
+ templater.manifest = env.manifest(type)
62
+ yield(templater, globalz) if block_given?
63
+ end
64
+ end
65
+
66
+ SUMMARY_TEMPLATE = %Q{<% if !entries.empty? && count > 1 %>
67
+ <%= env_key %>:
68
+ <% end %>
69
+ <% entries.each do |key, entry| %>
70
+ <%= key.ljust(width) %> # <%= entry.respond_to?(:comment) ? entry.comment : entry %>
71
+ <% end %>
72
+ }
73
+
74
+ def summarize(template=SUMMARY_TEMPLATE)
75
+ inspect(template, :width => 11, :count => 0) do |templater, globals|
76
+ width = globals[:width]
77
+ templater.entries = templater.manifest.minimap.collect! do |key, entry|
78
+ width = key.length if width < key.length
79
+ [key, entry]
80
+ end
81
+
82
+ globals[:width] = width
83
+ globals[:count] += 1 unless templater.entries.empty?
84
+ end
85
+ end
86
+
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,292 @@
1
+ module Tap
2
+ class Env
3
+
4
+ # Minimap adds minimization and search methods to an array of paths.
5
+ #
6
+ # paths = %w{
7
+ # path/to/file-0.1.0.txt
8
+ # path/to/file-0.2.0.txt
9
+ # path/to/another_file.txt
10
+ # }
11
+ # paths.extend Env::Minimap
12
+ #
13
+ # paths.minimatch('file') # => 'path/to/file-0.1.0.txt'
14
+ # paths.minimatch('file-0.2.0') # => 'path/to/file-0.2.0.txt'
15
+ # paths.minimatch('another_file') # => 'path/to/another_file.txt'
16
+ #
17
+ # More generally, Minimap may extend any object responding to each.
18
+ # Non-string entries are allowed; if the entry responds to minikey, then
19
+ # minimap will call that method to determine the 'path' to the entry.
20
+ # Otherwise, to_s is used. Override the entry_to_minikey method to
21
+ # change this default behavior.
22
+ #
23
+ # class ConstantMap < Array
24
+ # include Env::Minimap
25
+ #
26
+ # def entry_to_minikey(const)
27
+ # const.underscore
28
+ # end
29
+ # end
30
+ #
31
+ # constants = ConstantMap[Tap::Env::Minimap, Tap::Env]
32
+ # constants.minimatch('env') # => Tap::Env
33
+ # constants.minimatch('minimap') # => Tap::Env::Minimap
34
+ #
35
+ module Minimap
36
+
37
+ # Provides a minimized map of the entries using keys provided minikey.
38
+ #
39
+ # paths = %w{
40
+ # path/to/file-0.1.0.txt
41
+ # path/to/file-0.2.0.txt
42
+ # path/to/another_file.txt
43
+ # }.extend Minimap
44
+ #
45
+ # paths.minimap
46
+ # # => [
47
+ # # ['file-0.1.0', 'path/to/file-0.1.0.txt'],
48
+ # # ['file-0.2.0', 'path/to/file-0.2.0.txt'],
49
+ # # ['another_file','path/to/another_file.txt']]
50
+ #
51
+ def minimap
52
+ hash = {}
53
+ map = []
54
+ each {|entry| map << (hash[entry_to_minikey(entry)] = [entry]) }
55
+ minimize(hash.keys) do |key, mini_key|
56
+ hash[key].unshift mini_key
57
+ end
58
+
59
+ map
60
+ end
61
+
62
+ # Returns the first entry whose minikey mini-matches the input, or nil if
63
+ # no such entry exists.
64
+ #
65
+ # paths = %w{
66
+ # path/to/file-0.1.0.txt
67
+ # path/to/file-0.2.0.txt
68
+ # path/to/another_file.txt
69
+ # }.extend Minimap
70
+ #
71
+ # paths.minimatch('file-0.2.0') # => 'path/to/file-0.2.0.txt'
72
+ # paths.minimatch('file-0.3.0') # => nil
73
+ #
74
+ def minimatch(key)
75
+ key = key.to_s
76
+ each do |entry|
77
+ return entry if minimal_match?(entry_to_minikey(entry), key)
78
+ end
79
+ nil
80
+ end
81
+
82
+ # Returns minimap as a hash of (minikey, value) pairs.
83
+ def minihash(reverse=false)
84
+ hash = {}
85
+ minimap.each do |key, value|
86
+ if reverse
87
+ hash[value] = key
88
+ else
89
+ hash[key] = value
90
+ end
91
+ end
92
+ hash
93
+ end
94
+
95
+ protected
96
+
97
+ # A hook to convert entries to minikeys. Returns the entry by default,
98
+ # or entry.minikey if the entry responds to minikey.
99
+ def entry_to_minikey(entry)
100
+ entry.respond_to?(:minikey) ? entry.minikey : entry.to_s
101
+ end
102
+
103
+ module_function
104
+
105
+ # Minimizes a set of paths to the set of shortest basepaths that unqiuely
106
+ # identify the paths. The path extension and versions are removed from
107
+ # the basepath if possible. For example:
108
+ #
109
+ # Minimap.minimize ['path/to/a.rb', 'path/to/b.rb']
110
+ # # => ['a', 'b']
111
+ #
112
+ # Minimap.minimize ['path/to/a-0.1.0.rb', 'path/to/b-0.1.0.rb']
113
+ # # => ['a', 'b']
114
+ #
115
+ # Minimap.minimize ['path/to/file.rb', 'path/to/file.txt']
116
+ # # => ['file.rb', 'file.txt']
117
+ #
118
+ # Minimap.minimize ['path-0.1/to/file.rb', 'path-0.2/to/file.rb']
119
+ # # => ['path-0.1/to/file', 'path-0.2/to/file']
120
+ #
121
+ # Minimized paths that carry their extension will always carry
122
+ # their version as well, but the converse is not true; paths
123
+ # can be minimized to carry just the version and not the path
124
+ # extension.
125
+ #
126
+ # Minimap.minimize ['path/to/a-0.1.0.rb', 'path/to/a-0.1.0.txt']
127
+ # # => ['a-0.1.0.rb', 'a-0.1.0.txt']
128
+ #
129
+ # Minimap.minimize ['path/to/a-0.1.0.rb', 'path/to/a-0.2.0.rb']
130
+ # # => ['a-0.1.0', 'a-0.2.0']
131
+ #
132
+ # If a block is given, each (path, mini-path) pair will be passed
133
+ # to it after minimization.
134
+ def minimize(paths) # :yields: path, mini_path
135
+ unless block_given?
136
+ mini_paths = []
137
+ minimize(paths) {|p, mp| mini_paths << mp }
138
+ return mini_paths
139
+ end
140
+
141
+ splits = paths.uniq.collect do |path|
142
+ extname = File.extname(path)
143
+ extname = '' if extname =~ /^\.\d+$/
144
+ base = File.basename(path.chomp(extname))
145
+ version = base =~ /(-\d+(\.\d+)*)$/ ? $1 : ''
146
+
147
+ [dirname_or_array(path), base.chomp(version), extname, version, false, path]
148
+ end
149
+
150
+ while !splits.empty?
151
+ index = 0
152
+ splits = splits.collect do |(dir, base, extname, version, flagged, path)|
153
+ index += 1
154
+ case
155
+ when !flagged && just_one?(splits, index, base)
156
+
157
+ # found just one
158
+ yield(path, base)
159
+ nil
160
+ when dir.kind_of?(Array)
161
+
162
+ # no more path segments to use, try to add
163
+ # back version and extname
164
+ if dir.empty?
165
+ dir << File.dirname(base)
166
+ base = File.basename(base)
167
+ end
168
+
169
+ case
170
+ when !version.empty?
171
+ # add back version (occurs first)
172
+ [dir, "#{base}#{version}", extname, '', false, path]
173
+
174
+ when !extname.empty?
175
+
176
+ # add back extension (occurs second)
177
+ [dir, "#{base}#{extname}", '', version, false, path]
178
+ else
179
+
180
+ # nothing more to distinguish... path is minimized (occurs third)
181
+ yield(path, min_join(dir[0], base))
182
+ nil
183
+ end
184
+ else
185
+
186
+ # shift path segment. dirname_or_array returns an
187
+ # array if this is the last path segment to shift.
188
+ [dirname_or_array(dir), min_join(File.basename(dir), base), extname, version, false, path]
189
+ end
190
+ end.compact
191
+ end
192
+ end
193
+
194
+ # Returns true if the mini_path matches path. Matching logic reverses
195
+ # that of minimize:
196
+ #
197
+ # * a match occurs when path ends with mini_path
198
+ # * if mini_path doesn't specify an extension, then mini_path
199
+ # must only match path up to the path extension
200
+ # * if mini_path doesn't specify a version, then mini_path
201
+ # must only match path up to the path basename (minus the
202
+ # version and extname)
203
+ #
204
+ # For example:
205
+ #
206
+ # Minimap.minimal_match?('dir/file-0.1.0.rb', 'file') # => true
207
+ # Minimap.minimal_match?('dir/file-0.1.0.rb', 'dir/file') # => true
208
+ # Minimap.minimal_match?('dir/file-0.1.0.rb', 'file-0.1.0') # => true
209
+ # Minimap.minimal_match?('dir/file-0.1.0.rb', 'file-0.1.0.rb') # => true
210
+ #
211
+ # Minimap.minimal_match?('dir/file-0.1.0.rb', 'file.rb') # => false
212
+ # Minimap.minimal_match?('dir/file-0.1.0.rb', 'file-0.2.0') # => false
213
+ # Minimap.minimal_match?('dir/file-0.1.0.rb', 'another') # => false
214
+ #
215
+ # In matching, partial basenames are not allowed but partial directories
216
+ # are allowed. Hence:
217
+ #
218
+ # Minimap.minimal_match?('dir/file-0.1.0.txt', 'file') # => true
219
+ # Minimap.minimal_match?('dir/file-0.1.0.txt', 'ile') # => false
220
+ # Minimap.minimal_match?('dir/file-0.1.0.txt', 'r/file') # => true
221
+ #
222
+ def minimal_match?(path, mini_path)
223
+ extname = non_version_extname(mini_path)
224
+ version = mini_path =~ /(-\d+(\.\d+)*)#{extname}$/ ? $1 : ''
225
+
226
+ match_path = case
227
+ when !extname.empty?
228
+ # force full match
229
+ path
230
+ when !version.empty?
231
+ # match up to version
232
+ path.chomp(non_version_extname(path))
233
+ else
234
+ # match up base
235
+ path.chomp(non_version_extname(path)).sub(/(-\d+(\.\d+)*)$/, '')
236
+ end
237
+
238
+ # key ends with pattern AND basenames of each are equal...
239
+ # the last check ensures that a full path segment has
240
+ # been specified
241
+ match_path[-mini_path.length, mini_path.length] == mini_path && File.basename(match_path) == File.basename(mini_path)
242
+ end
243
+
244
+ # utility method for minimize -- joins the
245
+ # dir and path, preventing results like:
246
+ #
247
+ # "./path"
248
+ # "//path"
249
+ #
250
+ def min_join(dir, path) # :nodoc:
251
+ case dir
252
+ when "." then path
253
+ when "/" then "/#{path}"
254
+ else "#{dir}/#{path}"
255
+ end
256
+ end
257
+
258
+ # utility method for minimize -- returns the
259
+ # dirname of path, or an array if the dirname
260
+ # is effectively empty.
261
+ def dirname_or_array(path) # :nodoc:
262
+ dir = File.dirname(path)
263
+ case dir
264
+ when path, '.' then []
265
+ else dir
266
+ end
267
+ end
268
+
269
+ # utility method for minimize -- determines if there
270
+ # is just one of the base in splits, while flagging
271
+ # all matching entries.
272
+ def just_one?(splits, index, base) # :nodoc:
273
+ just_one = true
274
+ index.upto(splits.length-1) do |i|
275
+ if splits[i][1] == base
276
+ splits[i][4] = true
277
+ just_one = false
278
+ end
279
+ end
280
+
281
+ just_one
282
+ end
283
+
284
+ # utility method for minimal_match -- returns a non-version
285
+ # extname, or an empty string if the path ends in a version.
286
+ def non_version_extname(path) # :nodoc:
287
+ extname = File.extname(path)
288
+ extname =~ /^\.\d+$/ ? '' : extname
289
+ end
290
+ end
291
+ end
292
+ end