iostreams 0.20.3 → 1.0.0.beta

Sign up to get free protection for your applications and to get access to all the features.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/lib/io_streams/bzip2/reader.rb +9 -21
  3. data/lib/io_streams/bzip2/writer.rb +9 -21
  4. data/lib/io_streams/deprecated.rb +217 -0
  5. data/lib/io_streams/encode/reader.rb +12 -16
  6. data/lib/io_streams/encode/writer.rb +9 -13
  7. data/lib/io_streams/errors.rb +6 -6
  8. data/lib/io_streams/gzip/reader.rb +7 -14
  9. data/lib/io_streams/gzip/writer.rb +7 -15
  10. data/lib/io_streams/io_streams.rb +182 -524
  11. data/lib/io_streams/line/reader.rb +9 -9
  12. data/lib/io_streams/line/writer.rb +10 -11
  13. data/lib/io_streams/path.rb +190 -0
  14. data/lib/io_streams/paths/file.rb +176 -0
  15. data/lib/io_streams/paths/http.rb +92 -0
  16. data/lib/io_streams/paths/matcher.rb +61 -0
  17. data/lib/io_streams/paths/s3.rb +269 -0
  18. data/lib/io_streams/paths/sftp.rb +99 -0
  19. data/lib/io_streams/pgp.rb +47 -19
  20. data/lib/io_streams/pgp/reader.rb +20 -28
  21. data/lib/io_streams/pgp/writer.rb +24 -46
  22. data/lib/io_streams/reader.rb +28 -0
  23. data/lib/io_streams/record/reader.rb +20 -16
  24. data/lib/io_streams/record/writer.rb +28 -28
  25. data/lib/io_streams/row/reader.rb +22 -26
  26. data/lib/io_streams/row/writer.rb +29 -28
  27. data/lib/io_streams/stream.rb +400 -0
  28. data/lib/io_streams/streams.rb +125 -0
  29. data/lib/io_streams/symmetric_encryption/reader.rb +5 -13
  30. data/lib/io_streams/symmetric_encryption/writer.rb +16 -15
  31. data/lib/io_streams/tabular/header.rb +9 -3
  32. data/lib/io_streams/tabular/parser/array.rb +8 -3
  33. data/lib/io_streams/tabular/parser/csv.rb +6 -2
  34. data/lib/io_streams/tabular/parser/hash.rb +4 -1
  35. data/lib/io_streams/tabular/parser/json.rb +3 -1
  36. data/lib/io_streams/tabular/parser/psv.rb +3 -1
  37. data/lib/io_streams/tabular/utility/csv_row.rb +9 -8
  38. data/lib/io_streams/utils.rb +22 -0
  39. data/lib/io_streams/version.rb +1 -1
  40. data/lib/io_streams/writer.rb +28 -0
  41. data/lib/io_streams/xlsx/reader.rb +7 -19
  42. data/lib/io_streams/zip/reader.rb +7 -26
  43. data/lib/io_streams/zip/writer.rb +21 -38
  44. data/lib/iostreams.rb +15 -15
  45. data/test/bzip2_reader_test.rb +3 -3
  46. data/test/bzip2_writer_test.rb +3 -3
  47. data/test/deprecated_test.rb +123 -0
  48. data/test/encode_reader_test.rb +3 -3
  49. data/test/encode_writer_test.rb +6 -6
  50. data/test/gzip_reader_test.rb +2 -2
  51. data/test/gzip_writer_test.rb +3 -3
  52. data/test/io_streams_test.rb +43 -136
  53. data/test/line_reader_test.rb +20 -20
  54. data/test/line_writer_test.rb +3 -3
  55. data/test/path_test.rb +30 -28
  56. data/test/paths/file_test.rb +206 -0
  57. data/test/paths/http_test.rb +34 -0
  58. data/test/paths/matcher_test.rb +111 -0
  59. data/test/paths/s3_test.rb +207 -0
  60. data/test/pgp_reader_test.rb +8 -8
  61. data/test/pgp_writer_test.rb +13 -13
  62. data/test/record_reader_test.rb +5 -5
  63. data/test/record_writer_test.rb +4 -4
  64. data/test/row_reader_test.rb +5 -5
  65. data/test/row_writer_test.rb +6 -6
  66. data/test/stream_test.rb +116 -0
  67. data/test/streams_test.rb +255 -0
  68. data/test/utils_test.rb +20 -0
  69. data/test/xlsx_reader_test.rb +3 -3
  70. data/test/zip_reader_test.rb +12 -12
  71. data/test/zip_writer_test.rb +5 -5
  72. metadata +33 -45
  73. data/lib/io_streams/base_path.rb +0 -72
  74. data/lib/io_streams/file/path.rb +0 -58
  75. data/lib/io_streams/file/reader.rb +0 -12
  76. data/lib/io_streams/file/writer.rb +0 -22
  77. data/lib/io_streams/http/reader.rb +0 -71
  78. data/lib/io_streams/s3.rb +0 -26
  79. data/lib/io_streams/s3/path.rb +0 -40
  80. data/lib/io_streams/s3/reader.rb +0 -28
  81. data/lib/io_streams/s3/writer.rb +0 -85
  82. data/lib/io_streams/sftp/reader.rb +0 -67
  83. data/lib/io_streams/sftp/writer.rb +0 -68
  84. data/test/base_path_test.rb +0 -35
  85. data/test/file_path_test.rb +0 -97
  86. data/test/file_reader_test.rb +0 -33
  87. data/test/file_writer_test.rb +0 -50
  88. data/test/http_reader_test.rb +0 -38
  89. data/test/s3_reader_test.rb +0 -41
  90. data/test/s3_writer_test.rb +0 -41
@@ -0,0 +1,400 @@
1
+ module IOStreams
2
+ class Stream
3
+ attr_reader :io_stream
4
+ attr_writer :streams
5
+
6
+ def initialize(io_stream)
7
+ raise(ArgumentError, 'io_stream cannot be nil') if io_stream.nil?
8
+ raise(ArgumentError, "io_stream must not be a string: #{io_stream.inspect}") if io_stream.is_a?(String)
9
+
10
+ @io_stream = io_stream
11
+ @streams = nil
12
+ end
13
+
14
+ # Ignore the filename and use only the supplied streams.
15
+ #
16
+ # See #option to set an option for one of the streams included based on the file name extensions.
17
+ #
18
+ # Example:
19
+ #
20
+ # IOStreams.path('tempfile2527').stream(:zip).stream(:pgp, passphrase: 'receiver_passphrase').reader(&:read)
21
+ def stream(stream, **options)
22
+ streams.stream(stream, **options)
23
+ self
24
+ end
25
+
26
+ # Set the options for an element within the stream for this file.
27
+ # If the relevant stream is not found for this file it is ignored.
28
+ # For example, if the file does not have a pgp extension then the pgp option is not relevant.
29
+ #
30
+ # IOStreams.path('keep_safe.pgp').option(:pgp, passphrase: 'receiver_passphrase').reader(&:read)
31
+ #
32
+ # # In this case the file is not pgp so the `passphrase` option is ignored.
33
+ # IOStreams.path('keep_safe.enc').option(:pgp, passphrase: 'receiver_passphrase').reader(&:read)
34
+ #
35
+ # IOStreams.path(output_file_name).option(:pgp, passphrase: 'receiver_passphrase').reader(&:read)
36
+ def option(stream, **options)
37
+ streams.option(stream, **options)
38
+ self
39
+ end
40
+
41
+ # Adds the options for the specified stream as an option,
42
+ # but if streams have already been added it is instead added as a stream.
43
+ def option_or_stream(stream, **options)
44
+ streams.option_or_stream(stream, **options)
45
+ self
46
+ end
47
+
48
+ # Return the options already set for either a stream or option.
49
+ def setting(stream)
50
+ streams.setting(stream)
51
+ self
52
+ end
53
+
54
+ # Returns [Hash<Symbol:Hash>] the pipeline of streams
55
+ # with their options that will be applied when the reader or writer is invoked.
56
+ def pipeline
57
+ streams.pipeline
58
+ end
59
+
60
+ # Returns a Reader for reading a file / stream
61
+ #
62
+ # Parameters
63
+ # file_name_or_io [String|IO]
64
+ # The file_name of the file to write to, or an IO Stream that implements
65
+ # #read.
66
+ #
67
+ # streams [Symbol|Array]
68
+ # The formats/streams that be used to convert the data whilst it is
69
+ # being read.
70
+ # When nil, the file_name will be inspected to try and determine what
71
+ # streams should be applied.
72
+ # Default: nil
73
+ #
74
+ # file_name [String]
75
+ # When `streams` is not supplied, `file_name` can be used for determining the streams
76
+ # to apply to read the file/stream.
77
+ # This is particularly useful when `file_name_or_io` is a stream, or a temporary file name.
78
+ # Default: nil
79
+ #
80
+ # Example: Zip
81
+ # IOStreams.reader('myfile.zip') do |stream|
82
+ # puts stream.read
83
+ # end
84
+ #
85
+ # Example: Encrypted Zip
86
+ # IOStreams.reader('myfile.zip.enc') do |stream|
87
+ # puts stream.read
88
+ # end
89
+ #
90
+ # Example: Explicitly set the streams
91
+ # IOStreams.reader('myfile.zip.enc', [:zip, :enc]) do |stream|
92
+ # puts stream.read
93
+ # end
94
+ #
95
+ # Example: Supply custom options
96
+ # # Encrypt the file and get Symmetric Encryption to also compress it
97
+ # IOStreams.reader('myfile.csv.enc', streams: enc: {compress: true}) do |stream|
98
+ # puts stream.read
99
+ # end
100
+ #
101
+ # Note:
102
+ # * Passes the file_name_or_io as-is into the block if it is already a reader stream AND
103
+ # no streams are passed in.
104
+ #
105
+ def reader(&block)
106
+ streams.reader(io_stream, &block)
107
+ end
108
+
109
+ # Read an entire file into memory.
110
+ #
111
+ # Notes:
112
+ # - Use with caution since large files can cause a denial of service since
113
+ # this method will load the entire file into memory.
114
+ # - Recommend using instead `#reader`, `#each_line`, or `#each_record` to read a
115
+ # block into memory at a time.
116
+ def read(*args)
117
+ reader { |stream| stream.read(*args) }
118
+ end
119
+
120
+ # Copy from another stream, path, file_name or IO instance.
121
+ #
122
+ # Parameters:
123
+ # stream [IOStreams::Path|String<file_name>|IO]
124
+ # The stream to read from.
125
+ #
126
+ # :convert [true|false]
127
+ # Whether to apply the stream conversions during the copy.
128
+ # Default: true
129
+ #
130
+ # Examples:
131
+ #
132
+ # # Copy and convert streams based on file extensions
133
+ # IOStreams.path("target_file.json").copy_from("source_file_name.csv.gz")
134
+ #
135
+ # # Copy "as-is" without any automated stream conversions
136
+ # IOStreams.path("target_file.json").copy_from("source_file_name.csv.gz", convert: false)
137
+ #
138
+ # # Advanced copy with custom stream conversions on source and target.
139
+ # source = IOStreams.path("source_file").stream(encoding: "BINARY")
140
+ # IOStreams.path("target_file.pgp").option(:pgp, passphrase: "hello").copy_from(source)
141
+ def copy_from(source, convert: true)
142
+ if convert
143
+ stream = IOStreams.new(source)
144
+ streams.writer(io_stream) do |target|
145
+ stream.reader { |src| IO.copy_stream(src, target) }
146
+ end
147
+ else
148
+ stream = source.is_a?(Stream) ? source.dup : IOStreams.new(source)
149
+ streams.dup.stream(:none).writer(io_stream) do |target|
150
+ stream.stream(:none).reader { |src| IO.copy_stream(src, target) }
151
+ end
152
+ end
153
+ end
154
+
155
+ # Iterate over a file / stream returning one line at a time.
156
+ # Embedded lines (within double quotes) will be skipped if
157
+ # 1. The file name contains .csv
158
+ # 2. Or the embedded_within argument is set
159
+ #
160
+ # Example: Supply custom options
161
+ # IOStreams.each_line(file_name, embedded_within: '"') do |line|
162
+ # puts line
163
+ # end
164
+ #
165
+ def each_line(**args, &block)
166
+ # return enum_for __method__ unless block_given?
167
+ line_reader(**args) { |line_stream| line_stream.each(&block) }
168
+ end
169
+
170
+ # Iterate over a file / stream returning one line at a time.
171
+ # Embedded lines (within double quotes) will be skipped if
172
+ # 1. The file name contains .csv
173
+ # 2. Or the embedded_within argument is set
174
+ #
175
+ # Example: Supply custom options
176
+ # IOStreams.each_row(file_name, embedded_within: '"') do |line|
177
+ # puts line
178
+ # end
179
+ #
180
+ def each_row(**args, &block)
181
+ row_reader(**args) { |row_stream| row_stream.each(&block) }
182
+ end
183
+
184
+ # Returns [Hash] of every record in a file or stream with support for headers.
185
+ #
186
+ # Reading a delimited stream and converting to tabular form.
187
+ #
188
+ # Each record / line is returned one at a time so that very large files
189
+ # can be read without having to load the entire file into memory.
190
+ #
191
+ # Embedded lines (within double quotes) will be skipped if
192
+ # 1. The file name contains .csv
193
+ # 2. Or the embedded_within argument is set
194
+ #
195
+ # Example: Supply custom options
196
+ # IOStreams.each_record(file_name, embedded_within: '"') do |line|
197
+ # puts line
198
+ # end
199
+ #
200
+ # Example:
201
+ # file_name = 'customer_data.csv.pgp'
202
+ # IOStreams.each_record(file_name) do |hash|
203
+ # p hash
204
+ # end
205
+ def each_record(**args, &block)
206
+ record_reader(**args) { |record_stream| record_stream.each(&block) }
207
+ end
208
+
209
+ # Iterate over a file / stream returning each record/line one at a time.
210
+ # It will apply the embedded_within argument if the file or input_stream contain .csv in its name.
211
+ def line_reader(embedded_within: nil, **args)
212
+ embedded_within = '"' if embedded_within.nil? && streams.file_name&.include?('.csv')
213
+
214
+ reader { |io| yield IOStreams::Line::Reader.new(io, embedded_within: embedded_within, **args) }
215
+ end
216
+
217
+ # Iterate over a file / stream returning each line as an array, one at a time.
218
+ def row_reader(delimiter: nil, embedded_within: nil, **args)
219
+ line_reader(delimiter: delimiter, embedded_within: embedded_within) do |io|
220
+ yield IOStreams::Row::Reader.new(io, **args)
221
+ end
222
+ end
223
+
224
+ # Iterate over a file / stream returning each line as a hash, one at a time.
225
+ def record_reader(delimiter: nil, embedded_within: nil, **args)
226
+ line_reader(delimiter: delimiter, embedded_within: embedded_within) do |io|
227
+ yield IOStreams::Record::Reader.new(io, **args)
228
+ end
229
+ end
230
+
231
+ # Returns a Writer for writing to a file / stream
232
+ #
233
+ # Parameters
234
+ # file_name_or_io [String|IO]
235
+ # The file_name of the file to write to, or an IO Stream that implements
236
+ # #write.
237
+ #
238
+ # streams [Symbol|Array]
239
+ # The formats/streams that be used to convert the data whilst it is
240
+ # being written.
241
+ # When nil, the file_name will be inspected to try and determine what
242
+ # streams should be applied.
243
+ # Default: nil
244
+ #
245
+ # Stream types / extensions supported:
246
+ # .zip Zip File [ :zip ]
247
+ # .gz, .gzip GZip File [ :gzip ]
248
+ # .enc File Encrypted using symmetric encryption [ :enc ]
249
+ # other All other extensions will be returned as: [ :file ]
250
+ #
251
+ # When a file is encrypted, it may also be compressed:
252
+ # .zip.enc [ :zip, :enc ]
253
+ # .gz.enc [ :gz, :enc ]
254
+ #
255
+ # Example: Zip
256
+ # IOStreams.writer('myfile.zip') do |stream|
257
+ # stream.write(data)
258
+ # end
259
+ #
260
+ # Example: Encrypted Zip
261
+ # IOStreams.writer('myfile.zip.enc') do |stream|
262
+ # stream.write(data)
263
+ # end
264
+ #
265
+ # Example: Explicitly set the streams
266
+ # IOStreams.writer('myfile.zip.enc', [:zip, :enc]) do |stream|
267
+ # stream.write(data)
268
+ # end
269
+ #
270
+ # Example: Supply custom options
271
+ # IOStreams.writer('myfile.csv.enc', [enc: { compress: true }]) do |stream|
272
+ # stream.write(data)
273
+ # end
274
+ #
275
+ # Example: Set internal filename when creating a zip file
276
+ # IOStreams.writer('myfile.csv.zip', zip: { zip_file_name: 'myfile.csv' }) do |stream|
277
+ # stream.write(data)
278
+ # end
279
+ #
280
+ # Note:
281
+ # * Passes the file_name_or_io as-is into the block if it is already a writer stream AND
282
+ # no streams are passed in.
283
+ def writer(&block)
284
+ streams.writer(io_stream, &block)
285
+ end
286
+
287
+ # Write entire string to file.
288
+ #
289
+ # Notes:
290
+ # - Use with caution since preparing large amounts of data in memory can cause a denial of service
291
+ # since all the data for the file needs to be resident in memory before writing.
292
+ # - Recommend using instead `#writer`, `#line_writer`, or `#row_writer` to write a
293
+ # block of memory at a time.
294
+ def write(data)
295
+ writer { |stream| stream.write(data) }
296
+ end
297
+
298
+ def line_writer(**args)
299
+ writer { |io| yield IOStreams::Line::Writer.new(io, **args) }
300
+ end
301
+
302
+ def row_writer(delimiter: $/, **args)
303
+ line_writer(delimiter: delimiter) { |io| yield IOStreams::Row::Writer.new(io, **args) }
304
+ end
305
+
306
+ def record_writer(delimiter: $/, **args)
307
+ line_writer(delimiter: delimiter) { |io| yield IOStreams::Record::Writer.new(io, **args) }
308
+ end
309
+
310
+ # Set/get the original file_name
311
+ def file_name(file_name = :none)
312
+ file_name == :none ? streams.file_name : streams.file_name = file_name
313
+ self
314
+ end
315
+
316
+ # Set/get the original file_name
317
+ def file_name=(file_name)
318
+ streams.file_name = file_name
319
+ end
320
+
321
+ # Returns [String] the last component of this path.
322
+ # Returns `nil` if no `file_name` was set.
323
+ #
324
+ # Parameters:
325
+ # suffix: [String]
326
+ # When supplied the `suffix` is removed from the file_name before being returned.
327
+ # Use `.*` to remove any extension.
328
+ #
329
+ # IOStreams.path("/home/gumby/work/ruby.rb").basename #=> "ruby.rb"
330
+ # IOStreams.path("/home/gumby/work/ruby.rb").basename(".rb") #=> "ruby"
331
+ # IOStreams.path("/home/gumby/work/ruby.rb").basename(".*") #=> "ruby"
332
+ def basename(suffix = nil)
333
+ file_name = streams.file_name
334
+ return unless file_name
335
+
336
+ suffix.nil? ? ::File.basename(file_name) : ::File.basename(file_name, suffix)
337
+ end
338
+
339
+ # Returns [String] the directory for this file.
340
+ # Returns `nil` if no `file_name` was set.
341
+ #
342
+ # If `path` does not include a directory name the "." is returned.
343
+ #
344
+ # IOStreams.path("test.rb").dirname #=> "."
345
+ # IOStreams.path("a/b/d/test.rb").dirname #=> "a/b/d"
346
+ # IOStreams.path(".a/b/d/test.rb").dirname #=> ".a/b/d"
347
+ # IOStreams.path("foo.").dirname #=> "."
348
+ # IOStreams.path("test").dirname #=> "."
349
+ # IOStreams.path(".profile").dirname #=> "."
350
+ def dirname
351
+ file_name = streams.file_name
352
+ ::File.dirname(file_name) if file_name
353
+ end
354
+
355
+ # Returns [String] the extension for this file including the last period.
356
+ # Returns `nil` if no `file_name` was set.
357
+ #
358
+ # If `path` is a dotfile, or starts with a period, then the starting
359
+ # dot is not considered part of the extension.
360
+ #
361
+ # An empty string will also be returned when the period is the last character in the `path`.
362
+ #
363
+ # IOStreams.path("test.rb").extname #=> ".rb"
364
+ # IOStreams.path("a/b/d/test.rb").extname #=> ".rb"
365
+ # IOStreams.path(".a/b/d/test.rb").extname #=> ".rb"
366
+ # IOStreams.path("foo.").extname #=> ""
367
+ # IOStreams.path("test").extname #=> ""
368
+ # IOStreams.path(".profile").extname #=> ""
369
+ # IOStreams.path(".profile.sh").extname #=> ".sh"
370
+ def extname
371
+ file_name = streams.file_name
372
+ ::File.extname(file_name) if file_name
373
+ end
374
+
375
+ # Returns [String] the extension for this file _without_ the last period.
376
+ # Returns `nil` if no `file_name` was set.
377
+ #
378
+ # If `path` is a dotfile, or starts with a period, then the starting
379
+ # dot is not considered part of the extension.
380
+ #
381
+ # An empty string will also be returned when the period is the last character in the `path`.
382
+ #
383
+ # IOStreams.path("test.rb").extension #=> "rb"
384
+ # IOStreams.path("a/b/d/test.rb").extension #=> "rb"
385
+ # IOStreams.path(".a/b/d/test.rb").extension #=> "rb"
386
+ # IOStreams.path("foo.").extension #=> ""
387
+ # IOStreams.path("test").extension #=> ""
388
+ # IOStreams.path(".profile").extension #=> ""
389
+ # IOStreams.path(".profile.sh").extension #=> "sh"
390
+ def extension
391
+ extname&.sub(/^\./, '')
392
+ end
393
+
394
+ private
395
+
396
+ def streams
397
+ @streams ||= IOStreams::Streams.new
398
+ end
399
+ end
400
+ end
@@ -0,0 +1,125 @@
1
+ module IOStreams
2
+ class Streams
3
+ attr_accessor :file_name
4
+ attr_reader :streams, :options
5
+
6
+ def initialize(file_name = nil)
7
+ @file_name = file_name
8
+ @streams = nil
9
+ @options = nil
10
+ end
11
+
12
+ # Supply an option that is only applied once the file name extensions have been parsed.
13
+ # Note:
14
+ # - Cannot set both `stream` and `option`
15
+ def option(stream, **options)
16
+ stream = stream.to_sym unless stream.is_a?(Symbol)
17
+ raise(ArgumentError, "Invalid stream: #{stream.inspect}") unless IOStreams.extensions.include?(stream)
18
+ raise(ArgumentError, "Cannot call both #option and #stream on the same streams instance}") if @streams
19
+ raise(ArgumentError, "Cannot call #option unless the `file_name` was already set}") unless file_name
20
+
21
+ @options ||= {}
22
+ if opts = @options[stream]
23
+ opts.merge!(options)
24
+ else
25
+ @options[stream] = options.dup
26
+ end
27
+ self
28
+ end
29
+
30
+ def stream(stream, **options)
31
+ stream = stream.to_sym unless stream.is_a?(Symbol)
32
+ raise(ArgumentError, "Cannot call both #option and #stream on the same streams instance}") if @options
33
+
34
+ # To prevent any streams from being applied supply a stream named `:none`
35
+ if stream == :none
36
+ @streams = {}
37
+ return self
38
+ end
39
+ raise(ArgumentError, "Invalid stream: #{stream.inspect}") unless IOStreams.extensions.include?(stream)
40
+
41
+ @streams ||= {}
42
+ if opts = @streams[stream]
43
+ opts.merge!(options)
44
+ else
45
+ @streams[stream] = options.dup
46
+ end
47
+ self
48
+ end
49
+
50
+ def option_or_stream(stream, **options)
51
+ if streams
52
+ stream(stream, **options)
53
+ elsif file_name
54
+ option(stream, **options)
55
+ else
56
+ stream(stream, **options)
57
+ end
58
+ end
59
+
60
+ # Return the options set for either a stream or option.
61
+ def setting(stream)
62
+ return streams[stream] if streams
63
+ options[stream] if options
64
+ end
65
+
66
+ def reader(io_stream, &block)
67
+ execute(:reader, pipeline, io_stream, &block)
68
+ end
69
+
70
+ def writer(io_stream, &block)
71
+ execute(:writer, pipeline, io_stream, &block)
72
+ end
73
+
74
+ # Returns [Hash<Symbol:Hash>] the pipeline of streams
75
+ # with their options that will be applied when the reader or writer is invoked.
76
+ def pipeline
77
+ return streams.dup.freeze if streams
78
+ return {}.freeze unless file_name
79
+
80
+ built_streams = {}
81
+ # Encode stream is always first
82
+ built_streams[:encode] = options[:encode] if options&.key?(:encode)
83
+
84
+ opts = options || {}
85
+ parse_extensions.each { |stream| built_streams[stream] = opts[stream] || {} }
86
+ built_streams.freeze
87
+ end
88
+
89
+ private
90
+
91
+ def class_for_stream(type, stream)
92
+ ext = IOStreams.extensions[stream.nil? ? nil : stream.to_sym] || raise(ArgumentError, "Unknown Stream type: #{stream.inspect}")
93
+ ext.send("#{type}_class") || raise(ArgumentError, "No #{type} registered for Stream type: #{stream.inspect}")
94
+ end
95
+
96
+ # Returns the streams for the supplied file_name
97
+ def parse_extensions
98
+ parts = ::File.basename(file_name).split('.')
99
+ extensions = []
100
+ while extension = parts.pop
101
+ sym = extension.downcase.to_sym
102
+ break unless IOStreams.extensions[sym]
103
+
104
+ extensions.unshift(sym)
105
+ end
106
+ extensions
107
+ end
108
+
109
+ # Executes the streams that need to be executed.
110
+ def execute(type, pipeline, io_stream, &block)
111
+ raise(ArgumentError, 'IOStreams call is missing mandatory block') if block.nil?
112
+
113
+ if pipeline.empty?
114
+ block.call(io_stream)
115
+ elsif pipeline.size == 1
116
+ stream, opts = pipeline.first
117
+ class_for_stream(type, stream).stream(io_stream, opts, &block)
118
+ else
119
+ # Daisy chain multiple streams together
120
+ last = pipeline.keys.inject(block) { |inner, stream| ->(io) { class_for_stream(type, stream).stream(io, pipeline[stream], &inner) } }
121
+ last.call(io_stream)
122
+ end
123
+ end
124
+ end
125
+ end