imw 0.2.4 → 0.2.5

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -1,5 +1,5 @@
1
1
 
2
- = Overview
2
+ = What is the Infinite Monkeywrench?
3
3
 
4
4
  The Infinite Monkeywrench (IMW) is a Ruby frameworks to simplify the
5
5
  tasks of acquiring, extracting, transforming, loading, and packaging
@@ -23,7 +23,7 @@ data. It has the following goals:
23
23
  * Let you incorporate your own tools wherever you choose to.
24
24
 
25
25
  The Infinite Monkeywrench is a powerful tool but it is not always the
26
- right one to use. IMW is **not** designed for
26
+ right tool. IMW is **not** designed for
27
27
 
28
28
  * Scraping vast amounts of data (use Wuclan[http://github.com/infochimps/wuclan] and Monkeyshines[http://github.com/infochimps/monkeyshines])
29
29
 
@@ -33,14 +33,14 @@ right one to use. IMW is **not** designed for
33
33
 
34
34
  * Visualization
35
35
 
36
- = Setup
36
+ = Installation
37
37
 
38
38
  IMW is hosted on Gemcutter[http://gemcutter.org] so it's easy to install.
39
39
 
40
- You'll have to set up Gemcutter if you haven't already
40
+ You'll have to add <tt>http://gemcutter.org</tt> to your gem sources
41
+ if it isn't there already:
41
42
 
42
- $ sudo gem install gemcutter
43
- $ gem tumble
43
+ $ gem sources -a http://gemcutter.org
44
44
 
45
45
  and then install IMW
46
46
 
@@ -59,49 +59,82 @@ _anything_ with a URI and you create one using IMW.open.
59
59
 
60
60
  csv = IMW.open('/path/to/my_data.csv')
61
61
  html = IMW.open('http://www.infochimps.com')
62
- tar_bz2 = IMW.open(
63
62
 
64
63
  IMW dynamically extends a resource with modules appropriate to it when
65
64
  you open it. In the above case, +csv+ would be automatically extended
66
65
  by the IMW::Resources::Formats::Csv module, among others:
67
66
 
68
67
  csv.resource_modules
69
- => [IMW::Resources::LocalObj, IMW::Resources::LocalFile, IMW::Resources::Compressible, IMW::Resources::Formats::Csv]
68
+ => [IMW::Schemes::Local::Base, IMW::Schemes::Local::LocalFile, IMW::CompressedFiles::Compressible, IMW::Formats::Csv]
70
69
 
71
70
  while +html+ will use a different set
72
71
 
73
72
  html.resource_modules
74
- => [IMW::Resources::LocalObj, IMW::Resources::LocalFile, IMW::Resources::Compressible, IMW::Resources::Formats::Csv]
75
-
73
+ => [IMW::Schemes::Remote::Base, IMW::Schemes::Remote::RemoteFile, IMW::Schemes::HTTP, IMW::Formats::Html]
76
74
 
77
75
  Consult the documentation for the modules a resource uses to learn
78
76
  what it can do.
79
77
 
80
- Since resources are built around the idea of URIs, you can explicitly i
78
+ == Including/Excluding Resource Modules
79
+
80
+ You can exercise finer control of the resource modules IMW will extend
81
+ a given resource with by passing the <tt>:as</tt> and <tt>:without</tt>.
82
+
83
+ IMW.open('http://www.infochimps.com/some_raw_data', :without => [IMW::Formats::Html]).resource_modules
84
+ => [IMW::Schemes::Remote::Base, IMW::Schemes::Remote::RemoteFile, IMW::Schemes::HTTP]
85
+
86
+ IMW.open('http://www.infochimps.com', :as => [IMW::Formats::Json]).resource_modules
87
+ => [IMW::Schemes::Remote::Base, IMW::Schemes::Remote::RemoteFile, IMW::Schemes::HTTP, IMW::Formats::Json]
88
+
89
+ You can also pass <tt>:no_modules</tt> to not use any resource
90
+ modules.
91
+
92
+ == Handlers and Custom Resource Modules
93
+
94
+ IMW chooses which resource modules to extend an IMW::Resource by
95
+ iterating through an array of handlers, passing the resource to the
96
+ handler, and letting the handler's response (true/false) determine
97
+ whether or not to extend the resource with the module accompanying the
98
+ handler.
99
+
100
+ You can hook into this process by defining your own handlers. To
101
+ define a handler which should extend with +MyModule+ any resource with
102
+ a URI ending with <tt>.xxx</tt>
81
103
 
82
- == Manipulating Paths
104
+ IMW::Resource.register_handler MyModule, /\.xxx$/
83
105
 
84
- You can p
106
+ You can also use a Proc instead of a Regexp for more control. If the
107
+ result output of the Proc called with a resource is evaluates true
108
+ then the resource will be extended by +MyModule+.
109
+
110
+ IMW::Resource.register_handler MyModule, Proc.new { |resource| resource.is_local? && resource.path =~ /\.xxx$/ }
111
+
112
+ = Manipulating Paths
85
113
 
86
114
  IMW holds a registry of paths that you can define on the fly or store
87
- in a configuration file.
115
+ in a configuration file. Defining paths once in the registry and then
116
+ referring to them forever after by name helps keep your code flexible
117
+ as well as portable.
88
118
 
89
- IMW.add_path(:dropbox, "/var/www/public/dropbox")
90
- IMW.path_to(:dropbox) #=> "/var/www/public/dropbox"
119
+ IMW.add_path(:dropbox, "/var/www/public")
120
+ IMW.path_to(:dropbox)
121
+ => "/var/www/public"
91
122
 
92
- You can combine paths together dynamically.
123
+ You can combine named references together dynamically.
93
124
 
94
- IMW.add_path(:raw, "/data/raw")
95
- IMW.path_to(:raw, "my/dataset") #=> "/data/raw/my/dataset"
96
- IMW.add_path(:rejects, :raw, "rejects")
97
- IMW.path_to(:rejects) #=> "/data/raw/rejects"
125
+ IMW.add_path(:raw, :dropbox, "raw")
126
+ IMW.path_to(:raw)
127
+ => "/var/www/public/raw"
128
+ IMW.path_to(:raw, "my/dataset")
129
+ => "/var/www/public/raw/my/dataset
98
130
 
99
131
  Altering one path will update others
100
132
 
101
- IMW.add_path(:raw, "/data2/raw")
102
- IMW.path_to(:rejects) #=> "/data2/raw/rejects", not "/data/raw/rejects"
133
+ IMW.add_path(:dropbox, "/data") # redefines :raw
134
+ IMW.path_to(:raw, "my/dataset)
135
+ => "/data/raw/my/dataset" # not /var/www/public/raw/my/dataset
103
136
 
104
- == Files & Directories
137
+ = Files & Directories
105
138
 
106
139
  Use IMW.open to open files. The object returned by IMW.open obeys the
107
140
  usual semantics of a File object but it has new methods to manipulate
@@ -146,20 +179,21 @@ Files can readily be opened, read, and downloaded from the Internet
146
179
 
147
180
  == Archives & Compressed Files
148
181
 
149
- IMW works with a variety of archiving and compression programs (see
150
- IMW::EXTERNAL_PROGRAMS) to make packaging/unpackaging data easy.
182
+ IMW works with a variety of archiving and compression programs to make
183
+ packaging/unpackaging data easy.
151
184
 
152
185
  bz2 = IMW.open('/path/to/big_file.bz2')
153
186
  zip = IMW.open('/path/to/archive.zip')
154
187
  targz = IMW.open('/path/to/archive.tar.gz')
155
188
 
156
- # IMW recognizes files by extension
157
- bz2.archive? # false
158
- bz2.compressed? # true
159
- zip.archive? # true
160
- zip.compressed? # false
161
- targz.archive? # true
162
- targz.compressed? # true
189
+ IMW recognizes file properties by extension
190
+
191
+ bz2.is_archive? # false
192
+ bz2.is_compressed? # true
193
+ zip.is_archive? # true
194
+ zip.is_compressed? # false
195
+ targz.is_archive? # true
196
+ targz.is_compressed? # true
163
197
 
164
198
  # decompress or compress files
165
199
  big_file = bz2.decompress! # skip the ! to preserve the original
@@ -170,53 +204,113 @@ IMW::EXTERNAL_PROGRAMS) to make packaging/unpackaging data easy.
170
204
  tarbz2.extract # no need to decompress first
171
205
  new_tarbz2 = IMW.open!('/new/archive.tar').create(['/path1', '/path/2']).compress!
172
206
 
173
- == Data Formats
207
+ == Parsing and Emitting Data
174
208
 
175
- IMW encourages you to work with data as Ruby objects as much as
209
+ IMW encourages you to work with native Ruby data structures as much as
176
210
  possible by providing methods to parse common data formats directly
177
- into Ruby.
178
-
179
- The actual parsing is always handled by a separate library appropriate
180
- for the data format so it will be fast and, if you're familiar with
181
- the library, you can use many functions of the library directly on the
182
- object returned by IMW.open.
183
-
184
- IMW uses classes (defined in IMW::Files) to interface with each data
185
- type. The choice of class is determined by the extension of the path
186
- supplied to IMW.open.
187
-
188
- IMW.open('file.csv') #=> IMW::Files::Csv
189
- IMW.open('file.xml') #=> IMW::Files::Xml
190
- IMW.open('file.html') #=> IMW::Files::Html
191
-
192
- # default choice will be a text file
193
- IMW.open('strange_filename.wuzz') #=> IMW::Files::Text
194
-
195
- # but you force a particular choice
196
- IMW.open('strange_filename.wuzz', :as => :csv) #=> IMW::Files::Csv
197
-
198
- Some formats are extremely regular (CSV's, JSON, YAML, &c.) and can
199
- immediately be converted to simple Ruby objects. Other formats (flat
200
- files, HTML, XML, &c.) require parsing before they can be
201
- unambiguously converted to Ruby objects.
202
-
203
- As an example, consider flat, delimited files. They are extremely
204
- regular and IMW uses FasterCSV to automatically parse them into nested
205
- arrays, the only sensible and unambiguous Ruby representation of their
206
- data:
207
-
208
- delimit1 = IMW.open('/path/to/csv') # IMW::Files::Csv
209
- delimit1.entries #=> array of arrays of entries
210
- delimit1.each do |row|
211
- # passes in parsed rows
212
- ...
213
- end
214
-
215
- # if there's a funny delimiter, it can be passed as an option (in
216
- # this case identical to what would be passed to FasterCSV under the
217
- # hood
218
- delimit2 = IMW.open('/path/to/file.csv', :col_sep => " ")
219
-
211
+ into Arrays, Hashes and Strings.
212
+
213
+ Some data formats (CSV, JSON, YAML) have a structure which trivially
214
+ maps to Arrays, Hashes, and Strings and so these formats can
215
+ immediately be parsed.
216
+
217
+ Other formats (XML, HTML, flat files, &c.) use data structures which
218
+ do not map as readily to Arrays, Hashes, and Strings and so these will
219
+ have to be parsed first.
220
+
221
+ === Ruby-like Data Formats
222
+
223
+ These include delimited formats such as CSV and TSV as well as
224
+ "restricted tree-like" formats like JSON and YAML.
225
+
226
+ For the case of delimited data, consider the following CSV file:
227
+
228
+ ID,Name,Genus,Species
229
+ 001,Gray-bellied Night Monkey,Aotus,lemurinus
230
+ 002,Panamanian Night Monkey,Aotus,zonalis
231
+ 003,Hernández-Camacho's Night Monkey,Aotus,jorgehernandezi
232
+ 004,Gray-handed Night Monkey,Aotus,griseimembra
233
+ 005,Hershkovitz's Night Monkey,Aotus,hershkovitzi
234
+ 006,Brumback's Night Monkey,Aotus,brumbacki
235
+ 007,Three-striped Night Monkey,Aotus,trivirgatus
236
+ 008,Spix's Night Monkey,Aotus,vociferans
237
+ 009,Malaysian Lar Gibbon,Hylobates,lar lar
238
+ 010,Carpenter's Lar Gibbon,Hylobates,lar carpenteri
239
+
240
+ It trivially maps to an Array of Arrays:
241
+
242
+ data = IMW.open('/path/to/monkeys.csv').load
243
+ puts data.class
244
+ => Array
245
+ puts data.first.class
246
+ => Array
247
+ data.each { |row| puts row.inspect }
248
+ => ["ID", "Name", "Genus", "Species"]
249
+ ["001", "Gray-bellied Night Monkey", "Aotus", "lemurinus"]
250
+ ["002", "Panamanian Night Monkey", "Aotus", "zonalis"]
251
+ ...
252
+ ["010", "Carpenter's Lar Gibbon", "Hylobates", "lar carpenteri"]
253
+
254
+ Conversely, any array of arrays trivially maps to a delimited file.
255
+ Here we write out all rows where the genus is _Hylobates_ to a TSV
256
+ file:
257
+
258
+ hylobates = data.find_all { |row| row[2] == 'Hylobates' }
259
+ hylobates.dump('/path/to/monkeys.tsv')
260
+
261
+ IMW automatically formats the output as TSV and writes it to the
262
+ specified path.
263
+
264
+ Similarly, restricted tree-like formats like JSON and YAML, which map
265
+ cleanly onto Hashes, Arrays, and Strings, can also be automatically
266
+ parsed and emitted by IMW.
267
+
268
+ Consider a YAML version of the above CSV data:
269
+
270
+ - id: 001
271
+ name: Gray-bellied Night Monkey
272
+ genus: Aotus
273
+ species: lemurinus
274
+ - id: 002
275
+ name: Panamanian Night Monkey
276
+ genus: Aotus
277
+ species: zonalis
278
+ - id: 003
279
+ name: Hernández-Camacho's Night Monkey
280
+ genus: Aotus
281
+ species: jorgehernandezi
282
+ ...
283
+ - id: 010
284
+ name: Carpenter's Lar Gibbon
285
+ genus: Hylobates
286
+ species: lar carpenteri
287
+
288
+ This trivially maps to an Array of Hashes and so we can perform the
289
+ exact same filtration for YAML and JSON as we did for CSV and TSV (in
290
+ a one-liner!):
291
+
292
+ data = IMW.open('/path/to/monkeys.yaml').load
293
+ hylobates = data.map{ |monkey| monkey['genus'] == 'Hylobates' }
294
+ hylobates.dump('/path/to/monkeys.json')
295
+
296
+ Resources in these Ruby-like data formats also extend themselves with
297
+ Enumerable so goodies like +map+, +find_all+, &c. are available. This
298
+ enables converting YAML to JSON with a one-liner:
299
+
300
+ IMW.open('/path/to/monkeys.yaml').find_all { |monkey| monkey['genus'] == 'Hylobates' }.dump('/path/to/monkeys.json')
301
+
302
+ === Parsing More General Data Formats
303
+
304
+ Some data formats are structured but do not map readily to Hashes,
305
+ Arrays, and Strings (XML, HTML, &c.) while other data formats lack
306
+ structure or have a peculiar structure (flat files in arbitrary
307
+ syntax).
308
+
309
+ In both these cases the data needs to be parsed before it's usable.
310
+ For the XML and HTML type data formats, IMW uses Hpricot and the
311
+ IMW::Parsers::HtmlParser for parsing. For flat files, IMW provides
312
+ the IMW::Parsers::LineParser and the IMW::Parsers::RegexpParser.
313
+
220
314
  HTML files, on the other hand, are more complex and typically have to
221
315
  be parsed before being converted to plain Ruby objects:
222
316
 
@@ -245,17 +339,11 @@ rip::
245
339
  from the web, obtain it by querying databases, or use other services
246
340
  like rsync, ftp, &c. to pull it in from another computer.
247
341
 
248
- extract::
249
-
250
- Ripped data is often compressed or otherwise archived and needs to
251
- be extracted. It may also be sliced in many ways (excluding certain
252
- years, say) to reduce the volume to only what is required.
253
-
254
342
  parse::
255
343
 
256
344
  Data is parsed into Ruby objects and stored.
257
345
 
258
- munge::
346
+ fix::
259
347
 
260
348
  All the parsed data is combined, reconciled, and further processed
261
349
  into a final form.
@@ -268,7 +356,7 @@ package::
268
356
  Not all datasets
269
357
 
270
358
 
271
- == Datasets
359
+ = Datasets
272
360
 
273
361
  == Tasks & Dependencies
274
362
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.4
1
+ 0.2.5
@@ -11,6 +11,8 @@ module IMW
11
11
  # @abstract
12
12
  module Delimited
13
13
 
14
+ include Enumerable
15
+
14
16
  attr_accessor :delimited_settings
15
17
 
16
18
  # Return the data in this delimited resource as an array of
@@ -25,11 +27,9 @@ module IMW
25
27
  FasterCSV.parse(read, delimited_options, &block)
26
28
  end
27
29
 
28
- # Map each row in this delimited resource.
29
- #
30
- # @yield [Array] each row of the data
31
- def map &block
32
- load.map(&block)
30
+ # Call +block+ with each row in this delimited resource.
31
+ def each &block
32
+ load(&block)
33
33
  end
34
34
 
35
35
  # Dump an array of arrays into this resource.
@@ -4,37 +4,29 @@ module IMW
4
4
  # Defines methods for reading and writing JSON data.
5
5
  module Json
6
6
 
7
+ include Enumerable
8
+
7
9
  # Return the content of this resource.
8
10
  #
9
- # Will try to be smart about iterating over the data when
10
- # passed a block.
11
- #
12
- # - if the outermost JSON data structure is an array, then
13
- # yield each element
14
- #
15
- # - if the outermost JSON data structure is a mapping, then
16
- # yield each key, value pair
17
- #
18
- # - otherwise just yield the structure
11
+ # Will pass a block to the outermost JSON data structure's each
12
+ # method.
19
13
  #
20
14
  # @return [Hash, Array, String, Fixnum] whatever the JSON contained
21
15
  def load &block
22
16
  require 'json'
23
17
  json = JSON.parse(read)
24
18
  if block_given?
25
- case json
26
- when Array
27
- json.each { |obj| yield obj }
28
- when Hash
29
- json.each_pair { |key, value| yield key, value }
30
- else
31
- yield json
32
- end
19
+ json.each(&block)
33
20
  else
34
21
  json
35
22
  end
36
23
  end
37
24
 
25
+ # Iterate over the elements in the JSON.
26
+ def each &block
27
+ load(&block)
28
+ end
29
+
38
30
  # Dump the +data+ into this resource. It must be opened for
39
31
  # writing.
40
32
  #
@@ -4,37 +4,29 @@ module IMW
4
4
  # Provides methods for reading and writing YAML data.
5
5
  module Yaml
6
6
 
7
+ include Enumerable
8
+
7
9
  # Return the content of this resource.
8
10
  #
9
- # Will try to be smart about iterating over the data when
10
- # passed a block.
11
- #
12
- # - if the outermost YAML data structure is an array, then
13
- # yield each element
14
- #
15
- # - if the outermost YAML data structure is a mapping, then
16
- # yield each key, value pair
17
- #
18
- # - otherwise just yield the structure
11
+ # Will pass a block to the outermost YAML data structure's each
12
+ # method.
19
13
  #
20
14
  # @return [Hash, Array, String, Fixnum] whatever the YAML contained
21
15
  def load &block
22
16
  require 'yaml'
23
- yaml = YAML.load(read)
17
+ yaml = YAML.load(io)
24
18
  if block_given?
25
- case yaml
26
- when Array
27
- yaml.each { |obj| yield obj }
28
- when Hash
29
- yaml.each_pair { |key, value| yield key, value }
30
- else
31
- yield yaml
32
- end
19
+ yaml.each(&block)
33
20
  else
34
21
  yaml
35
22
  end
36
23
  end
37
24
 
25
+ # Iterate over the elements in the YAML.
26
+ def each &block
27
+ load(&block)
28
+ end
29
+
38
30
  # Dump the +data+ into this resource. It must be opened for
39
31
  # writing.
40
32
  #
data/lib/imw/resource.rb CHANGED
@@ -6,6 +6,31 @@ module IMW
6
6
  # URI handlers to IMW.
7
7
  USER_DEFINED_HANDLERS = [] unless defined?(USER_DEFINED_HANDLERS)
8
8
 
9
+ # Register a new resource handler which dynamically extends a new
10
+ # IMW::Resource with the given module +mod+.
11
+ #
12
+ # +handler+ must be one of
13
+ #
14
+ # 1. Regexp
15
+ # 2. Proc
16
+ # 3. +true+
17
+ #
18
+ # In case (1), if the regular expression matches the resource's URI
19
+ # then the module (+mod+) will be used to extend the resource.
20
+ #
21
+ # In case (2), if the Proc returns a value other than +false+ or
22
+ # +nil+ then the module will be used.
23
+ #
24
+ # In case (3), the module will be used.
25
+ #
26
+ # @param [String, Module] mod
27
+ # @param [Regexp, Proc, true] handler
28
+ def self.register_handler mod, handler
29
+ raise IMW::ArgumentError.new("Module must be either a Module or String") unless mod.is_a?(Module) || mod.is_a?(String)
30
+ raise IMW::ArgumentError.new("Handler must be either a Regexp, Proc, or true") unless handler.is_a?(Regexp) || handler.is_a?(Proc) || handler == true
31
+ self::USER_DEFINED_HANDLERS << [mod, handler]
32
+ end
33
+
9
34
  # A resource can be anything addressable via a URI. Examples
10
35
  # include local files, remote files, webpages, &c.
11
36
  #
@@ -178,6 +203,7 @@ module IMW
178
203
  raise IMW::Error.new([message, "No path defined for #{self.inspect} extended by #{resource_modules.join(' ')}"].compact.join(', ')) unless respond_to?(:path)
179
204
  raise IMW::Error.new([message, "No exist? method defined for #{self.inspect} extended by #{resource_modules.join(' ')}"].compact.join(', ')) unless respond_to?(:exist?)
180
205
  raise IMW::PathError.new([message, "#{path} does not exist"].compact.join(', ')) unless exist?
206
+ self
181
207
  end
182
208
 
183
209
  # Open a copy of this resource.
@@ -65,7 +65,7 @@ module IMW
65
65
  def dir
66
66
  IMW.open(dirname)
67
67
  end
68
-
68
+
69
69
  end
70
70
 
71
71
  # Defines methods for appropriate for a local file.
@@ -142,6 +142,29 @@ module IMW
142
142
  end
143
143
  io.close unless options[:persist]
144
144
  end
145
+
146
+ # Return a summary of properties of this local file.
147
+ #
148
+ # Returned properties include
149
+ # - basename
150
+ # - size
151
+ # - extension
152
+ # - snippet
153
+ def summary
154
+ {
155
+ :basename => basename,
156
+ :size => size,
157
+ :extension => extension,
158
+ :snippet => snippet
159
+ }
160
+ end
161
+
162
+ # Return a 1024-char snippet from this local file.
163
+ #
164
+ # @return [Array<String>]
165
+ def snippet
166
+ io.read(1024)
167
+ end
145
168
  end
146
169
 
147
170
  # Defines methods for manipulating the contents of a local
@@ -182,13 +205,6 @@ module IMW
182
205
  Dir[File.join(path, selector)]
183
206
  end
184
207
 
185
- # Return a list of all paths directly within this directory.
186
- #
187
- # @return [Array]
188
- def contents
189
- self['*']
190
- end
191
-
192
208
  # Does this directory contain +obj+?
193
209
  #
194
210
  # @param [String, IMW::Resource] obj
@@ -202,6 +218,13 @@ module IMW
202
218
  false
203
219
  end
204
220
 
221
+ # Return a list of all paths directly within this directory.
222
+ #
223
+ # @return [Array<String>]
224
+ def contents
225
+ self['*']
226
+ end
227
+
205
228
  # Return all paths within this directory, recursively.
206
229
  #
207
230
  # @return [Array<String>]
@@ -209,11 +232,17 @@ module IMW
209
232
  self['**/*']
210
233
  end
211
234
 
212
- # Return all resources within this directory, i.e. - all paths
213
- # converted to IMW::Resource objects.
235
+ # Return all resources directly within this directory.
214
236
  #
215
237
  # @return [Array<IMW::Resource>]
216
238
  def resources
239
+ contents.map { |path| IMW.open(path) }
240
+ end
241
+
242
+ # Return all resources within this directory, recursively.
243
+ #
244
+ # @return [Array<IMW::Resource>]
245
+ def all_resources
217
246
  all_contents.map do |path|
218
247
  IMW.open(path) unless File.directory?(path)
219
248
  end.compact
@@ -251,6 +280,26 @@ module IMW
251
280
  self
252
281
  end
253
282
 
283
+ # Return a hash summarizing this directory with a key
284
+ # <tt>:contents</tt> containing an array of hashes summarizing
285
+ # this directories contents.
286
+ #
287
+ # The directory summary includes the following information
288
+ # - basename
289
+ # - size
290
+ # - num_files
291
+ # - contents
292
+ #
293
+ # @return [Hash]
294
+ def summary
295
+ {
296
+ :basename => basename,
297
+ :size => size,
298
+ :num_files => contents.length,
299
+ :contents => resources.map { |resource| resource.summary }
300
+ }
301
+ end
302
+
254
303
  end
255
304
  end
256
305
  end