| Name | Total Lines | Lines of Code | Total Coverage | Code Coverage |
|---|---|---|---|---|
| lib/buildr/core/filter.rb | 404 | 199 | 12.62%
|
20.10%
|
Code reported as executed by Ruby looks like this...and this: this line is also marked as covered.Lines considered as run by rcov, but not reported by Ruby, look like this,and this: these lines were inferred by rcov (using simple heuristics).Finally, here's a line marked as not executed.
1 # Licensed to the Apache Software Foundation (ASF) under one or more |
2 # contributor license agreements. See the NOTICE file distributed with this |
3 # work for additional information regarding copyright ownership. The ASF |
4 # licenses this file to you under the Apache License, Version 2.0 (the |
5 # "License"); you may not use this file except in compliance with the License. |
6 # You may obtain a copy of the License at |
7 # |
8 # http://www.apache.org/licenses/LICENSE-2.0 |
9 # |
10 # Unless required by applicable law or agreed to in writing, software |
11 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
12 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
13 # License for the specific language governing permissions and limitations under |
14 # the License. |
15 |
16 |
17 require 'erb' |
18 |
19 |
20 module Buildr |
21 |
22 # A filter knows how to copy files from one directory to another, applying mappings to the |
23 # contents of these files. |
24 # |
25 # You can specify the mapping using a Hash, and it will map ${key} fields found in each source |
26 # file into the appropriate value in the target file. For example: |
27 # |
28 # filter.using 'version'=>'1.2', 'build'=>Time.now |
29 # |
30 # will replace all occurrences of <tt>${version}</tt> with <tt>1.2</tt>, and <tt>${build}</tt> |
31 # with the current date/time. |
32 # |
33 # You can also specify the mapping by passing a proc or a method, that will be called for |
34 # each source file, with the file name and content, returning the modified content. |
35 # |
36 # Without any mapping, the filter simply copies files from the source directory into the target |
37 # directory. |
38 # |
39 # A filter has one target directory, but you can specify any number of source directories, |
40 # either when creating the filter or calling #from. Include/exclude patterns are specified |
41 # relative to the source directories, so: |
42 # filter.include '*.png' |
43 # will only include PNG files from any of the source directories. |
44 # In the same way, you can use regular expressions, so: |
45 # filter.include /picture_.*\.png/ |
46 # will only include PNG files starting with picture_ from any of the sources directories. |
47 # |
48 # See Buildr#filter. |
49 class Filter |
50 |
51 def initialize #:nodoc: |
52 clear |
53 end |
54 |
55 # Returns the list of source directories (each being a file task). |
56 attr_reader :sources |
57 |
58 # :call-seq: |
59 # clear => self |
60 # |
61 # Clear filter sources and include/exclude patterns |
62 def clear |
63 @include = [] |
64 @exclude = [] |
65 @sources = FileList[] |
66 @mapper = Mapper.new |
67 self |
68 end |
69 |
70 # :call-seq: |
71 # from(*sources) => self |
72 # |
73 # Adds additional directories from which to copy resources. |
74 # |
75 # For example: |
76 # filter.from('src').into('target').using('build'=>Time.now) |
77 def from(*sources) |
78 @sources |= sources.flatten.map { |dir| file(File.expand_path(dir.to_s)) } |
79 self |
80 end |
81 |
82 # The target directory as a file task. |
83 def target |
84 return nil unless @target_dir |
85 unless @target |
86 @target = file(File.expand_path(@target_dir)) { |task| run if @target == task } |
87 @target.enhance @include.select {|f| f.is_a?(Rake::FileTask)} |
88 @target.enhance @exclude.select {|f| f.is_a?(Rake::FileTask)} |
89 @target.enhance copy_map.values |
90 end |
91 @target |
92 end |
93 |
94 # :call-seq: |
95 # into(dir) => self |
96 # |
97 # Sets the target directory into which files are copied and returns self. |
98 # |
99 # For example: |
100 # filter.from('src').into('target').using('build'=>Time.now) |
101 def into(dir) |
102 @target_dir = dir.to_s |
103 @target = nil |
104 self |
105 end |
106 |
107 # :call-seq: |
108 # include(*files) => self |
109 # |
110 # Specifies files to include and returns self. See FileList#include. |
111 # |
112 # By default all files are included. You can use this method to only include specific |
113 # files from the source directory. |
114 def include(*files) |
115 @include += files.flatten |
116 self |
117 end |
118 alias :add :include |
119 |
120 # :call-seq: |
121 # exclude(*files) => self |
122 # |
123 # Specifies files to exclude and returns self. See FileList#exclude. |
124 def exclude(*files) |
125 @exclude += files.flatten |
126 self |
127 end |
128 |
129 # The mapping. See #using. |
130 def mapping #:nodoc: |
131 @mapper.config |
132 end |
133 |
134 # The mapper to use. See #using. |
135 def mapper #:nodoc: |
136 @mapper.mapper_type |
137 end |
138 |
139 # :call-seq: |
140 # using(mapping) => self |
141 # using { |file_name, contents| ... } => self |
142 # |
143 # Specifies the mapping to use and returns self. |
144 # |
145 # The most typical mapping uses a Hash, and the default mapping uses the Maven style, so |
146 # <code>${key}</code> are mapped to the values. You can change that by passing a different |
147 # format as the first argument. Currently supports: |
148 # * :ant -- Map <code>@key@</code>. |
149 # * :maven -- Map <code>${key}</code> (default). |
150 # * :ruby -- Map <code>#{key}</code>. |
151 # * :erb -- Map <code><%= key %></code>. |
152 # * Regexp -- Maps the matched data (e.g. <code>/=(.*?)=/</code> |
153 # |
154 # For example: |
155 # filter.using 'version'=>'1.2' |
156 # Is the same as: |
157 # filter.using :maven, 'version'=>'1.2' |
158 # |
159 # You can also pass a proc or method. It will be called with the file name and content, |
160 # to return the mapped content. |
161 # |
162 # Without any mapping, all files are copied as is. |
163 # |
164 # To register new mapping type see the Mapper class. |
165 def using(*args, &block) |
166 @mapper.using(*args, &block) |
167 self |
168 end |
169 |
170 # :call-seq: |
171 # run => boolean |
172 # |
173 # Runs the filter. |
174 def run |
175 copy_map = copy_map() |
176 |
177 mkpath target.to_s |
178 return false if copy_map.empty? |
179 |
180 copy_map.each do |path, source| |
181 dest = File.expand_path(path, target.to_s) |
182 if File.directory?(source) |
183 mkpath dest |
184 else |
185 mkpath File.dirname(dest) |
186 if @mapper.mapper_type |
187 mapped = @mapper.transform(File.open(source, 'rb') { |file| file.read }, path) |
188 File.open(dest, 'wb') { |file| file.write mapped } |
189 else # no mapping |
190 cp source, dest |
191 end |
192 end |
193 File.chmod(File.stat(source).mode | 0200, dest) |
194 end |
195 touch target.to_s |
196 true |
197 end |
198 |
199 # Returns the target directory. |
200 def to_s |
201 target.to_s |
202 end |
203 |
204 protected |
205 |
206 # :call-seq: |
207 # pattern_match(file, pattern) => boolean |
208 # |
209 # This method returns true if the file name matches the pattern. |
210 # The pattern may be a String, a Regexp or a Proc. |
211 # |
212 def pattern_match(file, pattern) |
213 case |
214 when pattern.is_a?(Regexp) |
215 return file.match(pattern) |
216 when pattern.is_a?(String) |
217 return File.fnmatch(pattern, file) |
218 when pattern.is_a?(Proc) |
219 return pattern.call(file) |
220 when pattern.is_a?(Rake::FileTask) |
221 return pattern.to_s.match(file) |
222 else |
223 raise "Cannot interpret pattern #{pattern}" |
224 end |
225 end |
226 |
227 private |
228 def copy_map |
229 sources.each { |source| raise "Source directory #{source} doesn't exist" unless File.exist?(source.to_s) } |
230 raise 'No target directory specified, where am I going to copy the files to?' if target.nil? |
231 |
232 sources.flatten.map(&:to_s).inject({}) do |map, source| |
233 files = Util.recursive_with_dot_files(source). |
234 map { |file| Util.relative_path(file, source) }. |
235 select { |file| @include.empty? || @include.any? { |pattern| pattern_match(file, pattern) } }. |
236 reject { |file| @exclude.any? { |pattern| pattern_match(file, pattern) } } |
237 files.each do |file| |
238 src, dest = File.expand_path(file, source), File.expand_path(file, target.to_s) |
239 map[file] = src if !File.exist?(dest) || File.stat(src).mtime >= File.stat(dest).mtime |
240 end |
241 map |
242 end |
243 end |
244 |
245 # This class implements content replacement logic for Filter. |
246 # |
247 # To register a new template engine @:foo@, extend this class with a method like: |
248 # |
249 # def foo_transform(content, path = nil) |
250 # # if this method yields a key, the value comes from the mapping hash |
251 # content.gsub(/world/) { |str| yield :bar } |
252 # end |
253 # |
254 # Then you can use :foo mapping type on a Filter |
255 # |
256 # filter.using :foo, :bar => :baz |
257 # |
258 # Or all by your own, simply |
259 # |
260 # Mapper.new(:foo, :bar => :baz).transform("Hello world") # => "Hello baz" |
261 # |
262 # You can handle configuration arguments by providing a @*_config@ method like: |
263 # |
264 # # The return value of this method is available with the :config accessor. |
265 # def moo_config(*args, &block) |
266 # raise ArgumentError, "Expected moo block" unless block_given? |
267 # { :moos => args, :callback => block } |
268 # end |
269 # |
270 # def moo_transform(content, path = nil) |
271 # content.gsub(/moo+/i) do |str| |
272 # moos = yield :moos # same than config[:moos] |
273 # moo = moos[str.size - 3] || str |
274 # config[:callback].call(moo) |
275 # end |
276 # end |
277 # |
278 # Usage for the @:moo@ mapper would be something like: |
279 # |
280 # mapper = Mapper.new(:moo, 'ooone', 'twoo') do |str| |
281 # i = nil; str.capitalize.gsub(/\w/) { |s| s.send( (i = !i) ? 'upcase' : 'downcase' ) } |
282 # end |
283 # mapper.transform('Moo cow, mooo cows singing mooooo') # => 'OoOnE cow, TwOo cows singing MoOoOo' |
284 class Mapper |
285 |
286 attr_reader :mapper_type, :config |
287 |
288 def initialize(*args, &block) #:nodoc: |
289 using(*args, &block) |
290 end |
291 |
292 def using(*args, &block) |
293 case args.first |
294 when Hash # Maven hash mapping |
295 using :maven, *args |
296 when Binding # Erb binding |
297 using :erb, *args |
298 when Symbol # Mapping from a method |
299 raise ArgumentError, "Unknown mapping type: #{args.first}" unless respond_to?("#{args.first}_transform", true) |
300 configure(*args, &block) |
301 when Regexp # Mapping using a regular expression |
302 raise ArgumentError, 'Expected regular expression followed by mapping hash' unless args.size == 2 && Hash === args[1] |
303 @mapper_type, @config = *args |
304 else |
305 unless args.empty? && block.nil? |
306 raise ArgumentError, 'Expected proc, method or a block' if args.size > 1 || (args.first && block) |
307 @mapper_type = :callback |
308 config = args.first || block |
309 raise ArgumentError, 'Expected proc, method or callable' unless config.respond_to?(:call) |
310 @config = config |
311 end |
312 end |
313 self |
314 end |
315 |
316 def transform(content, path = nil) |
317 type = Regexp === mapper_type ? :regexp : mapper_type |
318 raise ArgumentError, "Invalid mapper type: #{type.inspect}" unless respond_to?("#{type}_transform", true) |
319 self.__send__("#{type}_transform", content, path) { |key| config[key] || config[key.to_s.to_sym] } |
320 end |
321 |
322 private |
323 def configure(mapper_type, *args, &block) |
324 configurer = method("#{mapper_type}_config") rescue nil |
325 if configurer |
326 @config = configurer.call(*args, &block) |
327 else |
328 raise ArgumentError, "Missing hash argument after :#{mapper_type}" unless args.size == 1 && Hash === args[0] |
329 @config = {} unless Hash === @config |
330 args.first.each_pair { |k, v| @config[k] = v.to_s } |
331 end |
332 @mapper_type = mapper_type |
333 end |
334 |
335 def maven_transform(content, path = nil) |
336 content.gsub(/\$\{.*?\}/) { |str| yield(str[2..-2]) || str } |
337 end |
338 |
339 def ant_transform(content, path = nil) |
340 content.gsub(/@.*?@/) { |str| yield(str[1..-2]) || str } |
341 end |
342 |
343 def ruby_transform(content, path = nil) |
344 content.gsub(/#\{.*?\}/) { |str| yield(str[2..-2]) || str } |
345 end |
346 |
347 def regexp_transform(content, path = nil) |
348 content.gsub(mapper_type) { |str| yield(str.scan(mapper_type).join) || str } |
349 end |
350 |
351 def callback_transform(content, path = nil) |
352 config.call(path, content) |
353 end |
354 |
355 def erb_transform(content, path = nil) |
356 case config |
357 when Binding |
358 bnd = config |
359 when Hash |
360 bnd = OpenStruct.new |
361 table = config.inject({}) { |h, e| h[e.first.to_sym] = e.last; h } |
362 bnd.instance_variable_set(:@table, table) |
363 bnd = bnd.instance_eval { binding } |
364 else |
365 bnd = config.instance_eval { binding } |
366 end |
367 require 'erb' |
368 ERB.new(content).result(bnd) |
369 end |
370 |
371 def erb_config(*args, &block) |
372 if block_given? |
373 raise ArgumentError, "Expected block or single argument, but both given." unless args.empty? |
374 block |
375 elsif args.size > 1 |
376 raise ArgumentError, "Expected block or single argument." |
377 else |
378 args.first |
379 end |
380 end |
381 |
382 end # class Mapper |
383 |
384 end |
385 |
386 # :call-seq: |
387 # filter(*source) => Filter |
388 # |
389 # Creates a filter that will copy files from the source directory(ies) into the target directory. |
390 # You can extend the filter to modify files by mapping <tt>${key}</tt> into values in each |
391 # of the copied files, and by including or excluding specific files. |
392 # |
393 # A filter is not a task, you must call the Filter#run method to execute it. |
394 # |
395 # For example, to copy all files from one directory to another: |
396 # filter('src/files').into('target/classes').run |
397 # To include only the text files, and replace each instance of <tt>${build}</tt> with the current |
398 # date/time: |
399 # filter('src/files').into('target/classes').include('*.txt').using('build'=>Time.now).run |
400 def filter(*sources) |
401 Filter.new.from(*sources) |
402 end |
403 |
404 end |
Generated on 2011-07-06 23:35:37 -0700 with rcov 0.9.8