iostreams 0.20.3 → 1.0.0.beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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