tap 0.12.4 → 0.17.0

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