imw 0.2.7 → 0.2.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. data/Gemfile +23 -0
  2. data/Gemfile.lock +47 -0
  3. data/LICENSE +20 -674
  4. data/README.rdoc +3 -4
  5. data/VERSION +1 -1
  6. data/lib/imw.rb +64 -35
  7. data/lib/imw/dataset.rb +12 -2
  8. data/lib/imw/formats.rb +4 -2
  9. data/lib/imw/formats/delimited.rb +96 -36
  10. data/lib/imw/formats/excel.rb +69 -101
  11. data/lib/imw/formats/json.rb +3 -5
  12. data/lib/imw/formats/pdf.rb +71 -0
  13. data/lib/imw/formats/yaml.rb +3 -5
  14. data/lib/imw/metadata.rb +66 -0
  15. data/lib/imw/metadata/contains_metadata.rb +44 -0
  16. data/lib/imw/metadata/dsl.rb +111 -0
  17. data/lib/imw/metadata/field.rb +65 -0
  18. data/lib/imw/metadata/schema.rb +227 -0
  19. data/lib/imw/metadata/schematized.rb +27 -0
  20. data/lib/imw/parsers.rb +1 -0
  21. data/lib/imw/parsers/flat.rb +44 -0
  22. data/lib/imw/resource.rb +36 -224
  23. data/lib/imw/schemes.rb +3 -1
  24. data/lib/imw/schemes/hdfs.rb +12 -1
  25. data/lib/imw/schemes/http.rb +1 -2
  26. data/lib/imw/schemes/local.rb +139 -16
  27. data/lib/imw/schemes/remote.rb +14 -9
  28. data/lib/imw/schemes/s3.rb +12 -0
  29. data/lib/imw/schemes/sql.rb +117 -0
  30. data/lib/imw/tools.rb +5 -3
  31. data/lib/imw/tools/downloader.rb +63 -0
  32. data/lib/imw/tools/summarizer.rb +21 -10
  33. data/lib/imw/utils.rb +10 -0
  34. data/lib/imw/utils/dynamically_extendable.rb +137 -0
  35. data/lib/imw/utils/error.rb +3 -0
  36. data/lib/imw/utils/extensions.rb +0 -4
  37. data/lib/imw/utils/extensions/array.rb +6 -7
  38. data/lib/imw/utils/extensions/hash.rb +3 -5
  39. data/lib/imw/utils/extensions/string.rb +3 -3
  40. data/lib/imw/utils/has_uri.rb +114 -0
  41. data/spec/data/{sample.csv → formats/delimited/sample.csv} +1 -1
  42. data/spec/data/{sample.tsv → formats/delimited/sample.tsv} +0 -0
  43. data/spec/data/formats/delimited/with_schema/ace-hardware-locations.tsv +11 -0
  44. data/spec/data/formats/delimited/with_schema/all-countries-ip-address-to-geolocation-data.tsv +16 -0
  45. data/spec/data/formats/delimited/with_schema/complete-list-of-starbucks-locations.tsv +11 -0
  46. data/spec/data/formats/delimited/with_schema/myspace-user-activity-stream-cumulative-word-count-from-from-dec.tsv +22 -0
  47. data/spec/data/formats/delimited/with_schema/myspace-user-activity-stream-myspace-application-adds-by-zip-cod.tsv +22 -0
  48. data/spec/data/formats/delimited/with_schema/myspace-user-activity-stream-myspace-application-counts.tsv +12 -0
  49. data/spec/data/formats/delimited/with_schema/myspace-user-activity-stream-user-count-by-latlong.tsv +13 -0
  50. data/spec/data/formats/delimited/with_schema/myspace-user-activity-stream-user-count-by-zip-code.tsv +22 -0
  51. data/spec/data/formats/delimited/with_schema/myspace-user-activity-stream-word-count-by-day-from-december-200.tsv +22 -0
  52. data/spec/data/formats/delimited/without_schema/ace-hardware-locations.tsv +10 -0
  53. data/spec/data/formats/delimited/without_schema/all-countries-ip-address-to-geolocation-data.tsv +15 -0
  54. data/spec/data/formats/delimited/without_schema/complete-list-of-starbucks-locations.tsv +10 -0
  55. data/spec/data/formats/delimited/without_schema/myspace-user-activity-stream-cumulative-word-count-from-from-dec.tsv +21 -0
  56. data/spec/data/formats/delimited/without_schema/myspace-user-activity-stream-myspace-application-adds-by-zip-cod.tsv +21 -0
  57. data/spec/data/formats/delimited/without_schema/myspace-user-activity-stream-myspace-application-counts.tsv +11 -0
  58. data/spec/data/formats/delimited/without_schema/myspace-user-activity-stream-user-count-by-latlong.tsv +12 -0
  59. data/spec/data/formats/delimited/without_schema/myspace-user-activity-stream-user-count-by-zip-code.tsv +21 -0
  60. data/spec/data/formats/delimited/without_schema/myspace-user-activity-stream-word-count-by-day-from-december-200.tsv +21 -0
  61. data/spec/data/formats/excel/sample.xls +0 -0
  62. data/spec/data/formats/json/sample.json +1 -0
  63. data/spec/data/formats/none/sample +650 -0
  64. data/spec/data/formats/sgml/sample.xml +617 -0
  65. data/spec/data/formats/text/sample.txt +650 -0
  66. data/spec/data/formats/yaml/sample.yaml +410 -0
  67. data/spec/data/schema-tabular.yaml +11 -0
  68. data/spec/imw/formats/delimited_spec.rb +34 -2
  69. data/spec/imw/formats/excel_spec.rb +55 -0
  70. data/spec/imw/formats/json_spec.rb +3 -3
  71. data/spec/imw/formats/sgml_spec.rb +4 -4
  72. data/spec/imw/formats/yaml_spec.rb +3 -3
  73. data/spec/imw/metadata/field_spec.rb +26 -0
  74. data/spec/imw/metadata/schema_spec.rb +27 -0
  75. data/spec/imw/metadata_spec.rb +39 -0
  76. data/spec/imw/parsers/line_parser_spec.rb +1 -1
  77. data/spec/imw/resource_spec.rb +0 -100
  78. data/spec/imw/schemes/hdfs_spec.rb +19 -13
  79. data/spec/imw/schemes/local_spec.rb +59 -3
  80. data/spec/imw/schemes/s3_spec.rb +4 -0
  81. data/spec/imw/utils/dynamically_extendable_spec.rb +69 -0
  82. data/spec/imw/utils/has_uri_spec.rb +55 -0
  83. data/spec/spec_helper.rb +1 -2
  84. data/spec/support/random.rb +4 -4
  85. metadata +58 -17
  86. data/CHANGELOG +0 -0
  87. data/TODO +0 -18
  88. data/spec/data/sample.json +0 -782
  89. data/spec/data/sample.txt +0 -131
  90. data/spec/data/sample.xml +0 -653
  91. data/spec/data/sample.yaml +0 -651
  92. data/spec/spec.opts +0 -4
  93. data/spec/support/extensions.rb +0 -18
@@ -0,0 +1,27 @@
1
+ module IMW
2
+ class Metadata
3
+ module Schematized
4
+
5
+ # The schema for this object.
6
+ #
7
+ # @return [IMW::Metadata::Schema, nil]
8
+ def schema
9
+ @schema
10
+ end
11
+
12
+ # Set a new schema for this object.
13
+ #
14
+ # Will call the object's +validate_schema!+ hook which should
15
+ # check the record and take the appropriate action if it's
16
+ # invalid.
17
+ #
18
+ # @param [Array, IMW::Metadata::Schema] new_schema
19
+ # @return [IMW::Metadata::Schema]
20
+ def schema= new_schema
21
+ @schema = IMW::Metadata::Schema.new(new_schema)
22
+ validate_schema! if respond_to?(:validate_schema!)
23
+ @schema
24
+ end
25
+ end
26
+ end
27
+ end
data/lib/imw/parsers.rb CHANGED
@@ -3,5 +3,6 @@ module IMW
3
3
  autoload :LineParser, 'imw/parsers/line_parser'
4
4
  autoload :RegexpParser, 'imw/parsers/regexp_parser'
5
5
  autoload :HtmlParser, 'imw/parsers/html_parser'
6
+ autoload :Flat, 'imw/parsers/flat'
6
7
  end
7
8
  end
@@ -0,0 +1,44 @@
1
+ module IMW
2
+ module Parsers
3
+
4
+ class Flat
5
+
6
+ attr_accessor :io
7
+ attr_accessor :state
8
+ attr_accessor :accumulated
9
+ attr_accessor :current
10
+
11
+ def initialize io
12
+ self.io = io
13
+ self.state = nil
14
+ self.accumulated = []
15
+ self.current = nil
16
+ end
17
+
18
+ def read_next!
19
+ self.current = io.readline.chomp
20
+ end
21
+
22
+ def parse!
23
+ while (! complete?)
24
+ read_next!
25
+ react_to_input!
26
+ end
27
+ end
28
+
29
+ def accumulate!
30
+ self.accumulated << current
31
+ end
32
+
33
+ def complete?
34
+ io.eof?
35
+ end
36
+
37
+ def react_to_input!
38
+ raise IMW::NotImplementedError.new("Override the `react_to_input!' method of the #{self.class} class")
39
+ end
40
+
41
+ end
42
+ end
43
+ end
44
+
data/lib/imw/resource.rb CHANGED
@@ -1,36 +1,7 @@
1
- require 'addressable/uri'
1
+ require 'imw/utils/has_uri'
2
2
 
3
3
  module IMW
4
4
 
5
- # Define this constant in your configuration file to add your own
6
- # URI handlers to IMW.
7
- USER_DEFINED_HANDLERS = [] unless defined?(USER_DEFINED_HANDLERS)
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
-
34
5
  # A resource can be anything addressable via a URI. Examples
35
6
  # include local files, remote files, webpages, &c.
36
7
  #
@@ -54,7 +25,7 @@ module IMW
54
25
  # The modules extending a particular IMW::Resource instance can be
55
26
  # listed as follows
56
27
  #
57
- # my_archive.resource_modules #=> [IMW::Local::Base, IMW::Local::File, IMW::Local::Compressible, IMW::Archives::Tarbz2]
28
+ # my_archive.modules #=> [IMW::Local::Base, IMW::Local::File, IMW::Local::Compressible, IMW::Archives::Tarbz2]
58
29
  #
59
30
  # By default, resources are opened for reading. Passing in the
60
31
  # appropriate <tt>:mode</tt> option changes this:
@@ -74,9 +45,6 @@ module IMW
74
45
  # accepts all the same arguments as IMW::Resource.new.
75
46
  class Resource
76
47
 
77
- # The URI object associated with this resource.
78
- attr_reader :uri
79
-
80
48
  # The mode in which to access this resource.
81
49
  attr_accessor :mode
82
50
 
@@ -85,142 +53,69 @@ module IMW
85
53
 
86
54
  # Create a new resource representing +uri+.
87
55
  #
88
- # IMW will automatically extend the resulting IMW::Resourcen
89
- # instance with modules appropriate to the given URI.
56
+ # IMW will automatically extend the resulting IMW::Resource
57
+ # instance with modules appropriate for the given URI:
90
58
  #
91
59
  # r = IMW::Resource.new("http://www.infochimps.com")
92
- # r.resource_modules
60
+ # r.modules
93
61
  # => [IMW::Schemes::Remote::Base, IMW::Schemes::Remote::RemoteFile, IMW::Schemes::HTTP, IMW::Formats::Html]
94
62
  #
95
63
  # You can prevent this altogether by passing in
96
64
  # <tt>:no_modules</tt>:
97
65
  #
98
- # r = IMW::Resource.new("http://www.infochimps.com")
99
- # r.resource_modules
100
- # => [IMW::Schemes::Remote::Base, IMW::Schemes::Remote::RemoteFile, IMW::Schemes::HTTP, IMW::Formats::Html]
66
+ # r = IMW::Resource.new("http://www.infochimps.com", :no_modules => true)
67
+ # r.modules
68
+ # => []
101
69
  #
102
70
  # And you can exert more fine-grained control with the
103
71
  # <tt>:use_modules</tt> and <tt>:skip_modules</tt> options, see
104
- # IMW::Resource.extend_resource! for details.
72
+ # IMW::Resource.extend_instance! for details.
105
73
  #
106
74
  # @param [String, Addressable::URI] uri
107
75
  # @param [Hash] options
108
76
  # @option options [true, false] no_modules
109
77
  # @option options [String] mode the mode to open the resource in (will be ignored when inapplicable)
78
+ # @option options [IMW::Metadata::Record, Array] schema the schema of this resource
110
79
  # @return [IMW::Resource]
111
80
  def initialize uri, options={}
112
81
  self.uri = uri
113
82
  self.resource_options = options
114
83
  self.mode = options[:mode] || 'r'
115
- extend_appropriately!(options) unless options[:no_modules]
116
- end
117
-
118
- # Return the modules this resource has been extended by.
119
- #
120
- # @return [Array] the modules this resource has been extended by.
121
- def resource_modules
122
- @resource_modules ||= []
123
- end
124
-
125
- # Works just like Object#extend except it keeps track of the
126
- # modules it has extended, see Resource#resource_modules.
127
- def extend mod
128
- resource_modules << mod
129
- super mod
130
- end
131
-
132
- # Extend this resource with modules by passing it through a
133
- # collection of handlers defined by IMW::Resource.handlers.
134
- #
135
- # Accepts the same options as Resource.extend_resource!.
136
- def extend_appropriately! options={}
137
- self.class.extend_resource!(self, options)
138
- end
139
-
140
- # Set the URI of this resource by parsing the given +uri+ (if
141
- # necessary).
142
- #
143
- # @param [String, Addressable::URI] uri the uri to parse
144
- def uri= uri
145
- if uri.is_a?(Addressable::URI)
146
- @uri = uri
147
- else
148
- begin
149
- @uri = Addressable::URI.parse(uri.to_s)
150
- rescue URI::InvalidURIError
151
- @uri = Addressable::URI.parse(URI.encode(uri.to_s))
152
- @encoded_uri = true
153
- end
154
- end
155
- end
156
-
157
- # The scheme of this resource. Will be +nil+ for local resources.
158
- #
159
- # @return [String]
160
- def scheme
161
- @scheme ||= uri.scheme
162
- end
163
-
164
- # The directory name of this resource's path.
165
- #
166
- # @return [String]
167
- def dirname
168
- @dirname ||= File.dirname(path)
169
- end
170
-
171
- # The basename of this resource's path.
172
- #
173
- # @return [String]
174
- def basename
175
- @basename ||= File.basename(path)
176
- end
177
-
178
- # Returns the extension (INCLUDING the '.') of this resource's
179
- # path. Redefine this in an including class for which this is
180
- # weird ('.tar.gz' I'm talking to you...)
181
- #
182
- # @return [String]
183
- def extname
184
- @extname ||= File.extname(path)
185
- end
186
-
187
- # Returns the extension (WITHOUT the '.') of this resource's path.
188
- #
189
- # @return [String]
190
- def extension
191
- @extension ||= extname[1..-1] || ''
192
- end
193
-
194
- # Returns the basename of the file with its extension removed
195
- #
196
- # IMW.open('/path/to/some_file.tar.gz').name # => some_file
197
- #
198
- # @return [String]
199
- def name
200
- @name ||= extname ? basename[0,basename.length - extname.length] : basename
84
+ self.schema = options[:schema] if options[:schema]
85
+ extend_appropriately!(options)
201
86
  end
202
87
 
203
- # Returns the user associated with the host of this URI.
204
- #
205
- # @return [String]
206
- def user
207
- @user ||= uri.user
208
- end
88
+ # Provides resources with a wrapped Addressable::URI object.
89
+ include IMW::Utils::HasURI
209
90
 
210
- def to_s
211
- uri.to_s
91
+ # Provides resources with a schema.
92
+ include IMW::Metadata::Schematized
93
+
94
+ # Gives IMW::Resource instances with the ability to dynamically
95
+ # extend themselves with modules chosen from a set of handlers
96
+ # stored by the IMW::Resource class.
97
+ include IMW::Utils::DynamicallyExtendable
98
+ [IMW::Schemes::HANDLERS, IMW::CompressedFiles::HANDLERS, IMW::Archives::HANDLERS, IMW::Formats::HANDLERS].each do |handlers|
99
+ register_handlers *handlers
212
100
  end
213
-
101
+
214
102
  # Raise an error unless this resource exists.
215
103
  #
216
104
  # @param [String] message an optional message to include
217
105
  def should_exist!(message=nil)
218
- raise IMW::Error.new([message, "No path defined for #{self.inspect} extended by #{resource_modules.join(' ')}"].compact.join(', ')) unless respond_to?(:path)
219
- raise IMW::Error.new([message, "No exist? method defined for #{self.inspect} extended by #{resource_modules.join(' ')}"].compact.join(', ')) unless respond_to?(:exist?)
220
- raise IMW::PathError.new([message, "#{path} does not exist"].compact.join(', ')) unless exist?
106
+ raise IMW::Error.new([message, "No path defined for #{self.inspect} extended by #{modules.join(' ')}"].compact.join(', ')) unless respond_to?(:path)
107
+ raise IMW::Error.new([message, "No exist? method defined for #{self.inspect} extended by #{modules.join(' ')}"].compact.join(', ')) unless respond_to?(:exist?)
108
+ raise IMW::PathError.new([message, "#{path} does not exist"].compact.join(', ')) unless exist?
221
109
  self
222
110
  end
223
111
 
112
+ # Close this resource.
113
+ #
114
+ # Modules should hook into super() as they need to redefine this
115
+ # method.
116
+ def close
117
+ end
118
+
224
119
  # Open a copy of this resource.
225
120
  #
226
121
  # This is useful when wanting to reset file handles. Though -- be
@@ -228,7 +123,7 @@ module IMW
228
123
  #
229
124
  # @return [IMW::Resource] the new (old) resource
230
125
  def reopen
231
- IMW.open(self.uri.to_s)
126
+ IMW.open(uri.to_s)
232
127
  end
233
128
 
234
129
  # If +method+ begins with the strings +is+, +on+, or +via+ and
@@ -257,92 +152,9 @@ module IMW
257
152
  # querying for a boolean response so answer false
258
153
  return false
259
154
  else
260
- raise IMW::NoMethodError, "undefined method `#{method}' for #{self}, extended by #{resource_modules.join(', ')}"
155
+ raise IMW::NoMethodError, "undefined method `#{method}' for #{self}, extended by #{modules.join(', ')}"
261
156
  end
262
157
  end
263
158
 
264
- # Iterate through IMW::Resource.handlers and extend the given
265
- # +resource+ with modules whose handler conditions match the
266
- # resource.
267
- #
268
- # Passing in <tt>:use_modules</tt> or <tt>:skip_modules</tt>
269
- # allows overriding the default behavior of handlers.
270
- #
271
- # @param [IMW::Resource] resource the resource to extend
272
- # @param [Hash] options
273
- # @option options [Array<String,Module>] use_modules a list of modules used regardless of handlers
274
- # @option options [Array<String,Module>] skip_modules a list of modules not to be used regardless of handlers
275
- # @return [IMW::Resource] the extended resource
276
- def self.extend_resource! resource, options={}
277
- options.reverse_merge!(:use_modules => [], :skip_modules => [])
278
- handlers.each do |mod_name, handler|
279
- case handler
280
- when Regexp then extend_resource_with_mod_or_string!(resource, mod_name, options[:skip_modules]) if handler =~ resource.uri.to_s
281
- when Proc then extend_resource_with_mod_or_string!(resource, mod_name, options[:skip_modules]) if handler.call(resource)
282
- when TrueClass then extend_resource_with_mod_or_string!(resource, mod_name, options[:skip_modules])
283
- else
284
- raise IMW::TypeError("A handler must be Regexp, Proc, or true")
285
- end
286
- end
287
- options[:use_modules].each { |mod_name| extend_resource_with_mod_or_string!(resource, mod_name, options[:skip_modules]) }
288
- resource
289
- end
290
-
291
- # A list of handlers to match against each new resource.
292
- #
293
- # When an IMW::Resource is instantiated it eventually calls
294
- # IMW::Resource.extend_resource! which will iterate through the
295
- # handlers in IMW::Resource.handlers, extending the resource with
296
- # modules whose handler conditions are satisfied.
297
- #
298
- # A handler is just an Array with two elements. The first should be
299
- # a module or a string identifying a module.
300
- #
301
- # If the second element is a Regexp, the corresponding module will
302
- # be used if the regexp matches the resource's URI (as a string)
303
- #
304
- # If the second element is a Proc, it will be called with the
305
- # resource as its only argument and if it returns true then the
306
- # module will be used.
307
- #
308
- # You can define your own handlers by appending them to
309
- # IMW::Resource::USER_DEFINED_HANDLERS in your <tt>.imwrc</tt>
310
- # file.
311
- #
312
- # The order in which handlers appear is significant --
313
- # IMW::CompressedFiles::HANDLERS must be _before_
314
- # IMW::Archives::HANDLERS, for example, because of (say)
315
- # <tt>.tar.bz2</tt> files.
316
- #
317
- # @return [Array]
318
- def self.handlers
319
- # order is important!
320
- #
321
- #
322
- #
323
- #CompressedFiles must come before
324
- # Archives because of tar.bz2 type files
325
- IMW::Schemes::HANDLERS + IMW::CompressedFiles::HANDLERS + IMW::Archives::HANDLERS + IMW::Formats::HANDLERS + USER_DEFINED_HANDLERS
326
- end
327
-
328
- protected
329
- # Extend +resource+ with +mod_or_string+. Will work hard to try
330
- # and interpret +mod_or_string+ as a module if it's a string.
331
- #
332
- # @param [IMW::Resource] resource the resource to extend
333
- #
334
- # @param [Module, String] mod_or_string the module or string
335
- # representing a module to extend the resource with
336
- #
337
- # @param [Array<Module,String>] skip_modules modules to exclude
338
- def self.extend_resource_with_mod_or_string! resource, mod_or_string, skip_modules
339
- return if skip_modules.include?(mod_or_string)
340
- if mod_or_string.is_a?(Module)
341
- resource.extend(mod_or_string)
342
- else
343
- m = IMW.class_eval(mod_or_string)
344
- resource.extend(m) unless skip_modules.include?(m)
345
- end
346
- end
347
159
  end
348
160
  end
data/lib/imw/schemes.rb CHANGED
@@ -6,6 +6,7 @@ module IMW
6
6
  autoload :HTTP, 'imw/schemes/http'
7
7
  autoload :HTTPS, 'imw/schemes/http'
8
8
  autoload :HDFS, 'imw/schemes/hdfs'
9
+ autoload :SQL, 'imw/schemes/sql'
9
10
 
10
11
  HANDLERS = [
11
12
  ["Schemes::Local::Base", Proc.new { |resource| resource.scheme == 'file' || resource.scheme.blank? } ],
@@ -13,7 +14,8 @@ module IMW
13
14
  ["Schemes::S3", %r{^s3://}i ],
14
15
  ["Schemes::HTTP", %r{^http://}i ],
15
16
  ["Schemes::HTTPS", %r{^https://}i ],
16
- ["Schemes::HDFS", %r{^hdfs://}i ]
17
+ ["Schemes::HDFS", %r{^hdfs://}i ],
18
+ ["Schemes::SQL::Base", %r{^\w+sql://}i ]
17
19
  ]
18
20
  end
19
21
  end
@@ -234,7 +234,18 @@ module IMW
234
234
  def resources
235
235
  contents.map { |path| IMW.open(path) }
236
236
  end
237
-
237
+
238
+ # Return the resource at the base path of this resource joined
239
+ # to +path+.
240
+ #
241
+ # IMW.open('hdfs:///path/to/dir').join('subdir')
242
+ # #=> IMW::Resource at 'hdfs:///path/to/dir/subdir'
243
+ #
244
+ # @param [Array<String>] paths
245
+ # @return [IMW::Resource]
246
+ def join *paths
247
+ IMW.open(File.join(stripped_uri.to_s, *paths))
248
+ end
238
249
  end
239
250
  end
240
251
  end