C0 code coverage information
Generated on Wed Oct 07 08:33:57 -0700 2009 with rcov 0.8.2.1
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 #
45 # See Buildr#filter.
46 class Filter
47
48 def initialize #:nodoc:
49 clear
50 end
51
52 # Returns the list of source directories (each being a file task).
53 attr_reader :sources
54
55 # :call-seq:
56 # clear => self
57 #
58 # Clear filter sources and include/exclude patterns
59 def clear
60 @include = []
61 @exclude = []
62 @sources = FileList[]
63 @mapper = Mapper.new
64 self
65 end
66
67 # :call-seq:
68 # from(*sources) => self
69 #
70 # Adds additional directories from which to copy resources.
71 #
72 # For example:
73 # filter.from('src').into('target').using('build'=>Time.now)
74 def from(*sources)
75 @sources |= sources.flatten.map { |dir| file(File.expand_path(dir.to_s)) }
76 self
77 end
78
79 # The target directory as a file task.
80 attr_reader :target
81
82 # :call-seq:
83 # into(dir) => self
84 #
85 # Sets the target directory into which files are copied and returns self.
86 #
87 # For example:
88 # filter.from('src').into('target').using('build'=>Time.now)
89 def into(dir)
90 @target = file(File.expand_path(dir.to_s)) { |task| run if target == task }
91 self
92 end
93
94 # :call-seq:
95 # include(*files) => self
96 #
97 # Specifies files to include and returns self. See FileList#include.
98 #
99 # By default all files are included. You can use this method to only include specific
100 # files from the source directory.
101 def include(*files)
102 @include += files
103 self
104 end
105 alias :add :include
106
107 # :call-seq:
108 # exclude(*files) => self
109 #
110 # Specifies files to exclude and returns self. See FileList#exclude.
111 def exclude(*files)
112 @exclude += files
113 self
114 end
115
116 # The mapping. See #using.
117 def mapping #:nodoc:
118 @mapper.config
119 end
120
121 # The mapper to use. See #using.
122 def mapper #:nodoc:
123 @mapper.mapper_type
124 end
125
126 # :call-seq:
127 # using(mapping) => self
128 # using { |file_name, contents| ... } => self
129 #
130 # Specifies the mapping to use and returns self.
131 #
132 # The most typical mapping uses a Hash, and the default mapping uses the Maven style, so
133 # <code>${key}</code> are mapped to the values. You can change that by passing a different
134 # format as the first argument. Currently supports:
135 # * :ant -- Map <code>@key@</code>.
136 # * :maven -- Map <code>${key}</code> (default).
137 # * :ruby -- Map <code>#{key}</code>.
138 # * :erb -- Map <code><%= key %></code>.
139 # * Regexp -- Maps the matched data (e.g. <code>/=(.*?)=/</code>
140 #
141 # For example:
142 # filter.using 'version'=>'1.2'
143 # Is the same as:
144 # filter.using :maven, 'version'=>'1.2'
145 #
146 # You can also pass a proc or method. It will be called with the file name and content,
147 # to return the mapped content.
148 #
149 # Without any mapping, all files are copied as is.
150 #
151 # To register new mapping type see the Mapper class.
152 def using(*args, &block)
153 @mapper.using(*args, &block)
154 self
155 end
156
157 # :call-seq:
158 # run => boolean
159 #
160 # Runs the filter.
161 def run
162 sources.each { |source| raise "Source directory #{source} doesn't exist" unless File.exist?(source.to_s) }
163 raise 'No target directory specified, where am I going to copy the files to?' if target.nil?
164
165 copy_map = sources.flatten.map(&:to_s).inject({}) do |map, source|
166 files = Util.recursive_with_dot_files(source).
167 map { |file| Util.relative_path(file, source) }.
168 select { |file| @include.empty? || @include.any? { |pattern| File.fnmatch(pattern, file) } }.
169 reject { |file| @exclude.any? { |pattern| File.fnmatch(pattern, file) } }
170 files.each do |file|
171 src, dest = File.expand_path(file, source), File.expand_path(file, target.to_s)
172 map[file] = src if !File.exist?(dest) || File.stat(src).mtime >= File.stat(dest).mtime
173 end
174 map
175 end
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 File.chmod(0664, dest)
192 end
193 end
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 # This class implements content replacement logic for Filter.
205 #
206 # To register a new template engine @:foo@, extend this class with a method like:
207 #
208 # def foo_transform(content, path = nil)
209 # # if this method yields a key, the value comes from the mapping hash
210 # content.gsub(/world/) { |str| yield :bar }
211 # end
212 #
213 # Then you can use :foo mapping type on a Filter
214 #
215 # filter.using :foo, :bar => :baz
216 #
217 # Or all by your own, simply
218 #
219 # Mapper.new(:foo, :bar => :baz).transform("Hello world") # => "Hello baz"
220 #
221 # You can handle configuration arguments by providing a @*_config@ method like:
222 #
223 # # The return value of this method is available with the :config accessor.
224 # def moo_config(*args, &block)
225 # raise ArgumentError, "Expected moo block" unless block_given?
226 # { :moos => args, :callback => block }
227 # end
228 #
229 # def moo_transform(content, path = nil)
230 # content.gsub(/moo+/i) do |str|
231 # moos = yield :moos # same than config[:moos]
232 # moo = moos[str.size - 3] || str
233 # config[:callback].call(moo)
234 # end
235 # end
236 #
237 # Usage for the @:moo@ mapper would be something like:
238 #
239 # mapper = Mapper.new(:moo, 'ooone', 'twoo') do |str|
240 # i = nil; str.capitalize.gsub(/\w/) { |s| s.send( (i = !i) ? 'upcase' : 'downcase' ) }
241 # end
242 # mapper.transform('Moo cow, mooo cows singing mooooo') # => 'OoOnE cow, TwOo cows singing MoOoOo'
243 class Mapper
244
245 attr_reader :mapper_type, :config
246
247 def initialize(*args, &block) #:nodoc:
248 using(*args, &block)
249 end
250
251 def using(*args, &block)
252 case args.first
253 when Hash # Maven hash mapping
254 using :maven, *args
255 when Binding # Erb binding
256 using :erb, *args
257 when Symbol # Mapping from a method
258 raise ArgumentError, "Unknown mapping type: #{args.first}" unless respond_to?("#{args.first}_transform", true)
259 configure(*args, &block)
260 when Regexp # Mapping using a regular expression
261 raise ArgumentError, 'Expected regular expression followed by mapping hash' unless args.size == 2 && Hash === args[1]
262 @mapper_type, @config = *args
263 else
264 unless args.empty? && block.nil?
265 raise ArgumentError, 'Expected proc, method or a block' if args.size > 1 || (args.first && block)
266 @mapper_type = :callback
267 config = args.first || block
268 raise ArgumentError, 'Expected proc, method or callable' unless config.respond_to?(:call)
269 @config = config
270 end
271 end
272 self
273 end
274
275 def transform(content, path = nil)
276 type = Regexp === mapper_type ? :regexp : mapper_type
277 raise ArgumentError, "Invalid mapper type: #{type.inspect}" unless respond_to?("#{type}_transform", true)
278 self.__send__("#{type}_transform", content, path) { |key| config[key] || config[key.to_s.to_sym] }
279 end
280
281 private
282 def configure(mapper_type, *args, &block)
283 configurer = method("#{mapper_type}_config") rescue nil
284 if configurer
285 @config = configurer.call(*args, &block)
286 else
287 raise ArgumentError, "Missing hash argument after :#{mapper_type}" unless args.size == 1 && Hash === args[0]
288 @config = args.first
289 end
290 @mapper_type = mapper_type
291 end
292
293 def maven_transform(content, path = nil)
294 content.gsub(/\$\{.*?\}/) { |str| yield(str[2..-2]) || str }
295 end
296
297 def ant_transform(content, path = nil)
298 content.gsub(/@.*?@/) { |str| yield(str[1..-2]) || str }
299 end
300
301 def ruby_transform(content, path = nil)
302 content.gsub(/#\{.*?\}/) { |str| yield(str[2..-2]) || str }
303 end
304
305 def regexp_transform(content, path = nil)
306 content.gsub(mapper_type) { |str| yield(str.scan(mapper_type).join) || str }
307 end
308
309 def callback_transform(content, path = nil)
310 config.call(path, content)
311 end
312
313 def erb_transform(content, path = nil)
314 case config
315 when Binding
316 bnd = config
317 when Hash
318 bnd = OpenStruct.new
319 table = config.inject({}) { |h, e| h[e.first.to_sym] = e.last; h }
320 bnd.instance_variable_set(:@table, table)
321 bnd = bnd.instance_eval { binding }
322 else
323 bnd = config.instance_eval { binding }
324 end
325 require 'erb'
326 ERB.new(content).result(bnd)
327 end
328
329 def erb_config(*args, &block)
330 if block_given?
331 raise ArgumentError, "Expected block or single argument, but both given." unless args.empty?
332 block
333 elsif args.size > 1
334 raise ArgumentError, "Expected block or single argument."
335 else
336 args.first
337 end
338 end
339
340 end # class Mapper
341
342 end
343
344 # :call-seq:
345 # filter(*source) => Filter
346 #
347 # Creates a filter that will copy files from the source directory(ies) into the target directory.
348 # You can extend the filter to modify files by mapping <tt>${key}</tt> into values in each
349 # of the copied files, and by including or excluding specific files.
350 #
351 # A filter is not a task, you must call the Filter#run method to execute it.
352 #
353 # For example, to copy all files from one directory to another:
354 # filter('src/files').into('target/classes').run
355 # To include only the text files, and replace each instance of <tt>${build}</tt> with the current
356 # date/time:
357 # filter('src/files').into('target/classes').include('*.txt').using('build'=>Time.now).run
358 def filter(*sources)
359 Filter.new.from(*sources)
360 end
361
362 end
Generated using the rcov code coverage analysis tool for Ruby
version 0.8.2.1.