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.
- checksums.yaml +4 -4
- data/lib/io_streams/bzip2/reader.rb +9 -21
- data/lib/io_streams/bzip2/writer.rb +9 -21
- data/lib/io_streams/deprecated.rb +217 -0
- data/lib/io_streams/encode/reader.rb +12 -16
- data/lib/io_streams/encode/writer.rb +9 -13
- data/lib/io_streams/errors.rb +6 -6
- data/lib/io_streams/gzip/reader.rb +7 -14
- data/lib/io_streams/gzip/writer.rb +7 -15
- data/lib/io_streams/io_streams.rb +182 -524
- data/lib/io_streams/line/reader.rb +9 -9
- data/lib/io_streams/line/writer.rb +10 -11
- data/lib/io_streams/path.rb +190 -0
- data/lib/io_streams/paths/file.rb +176 -0
- data/lib/io_streams/paths/http.rb +92 -0
- data/lib/io_streams/paths/matcher.rb +61 -0
- data/lib/io_streams/paths/s3.rb +269 -0
- data/lib/io_streams/paths/sftp.rb +99 -0
- data/lib/io_streams/pgp.rb +47 -19
- data/lib/io_streams/pgp/reader.rb +20 -28
- data/lib/io_streams/pgp/writer.rb +24 -46
- data/lib/io_streams/reader.rb +28 -0
- data/lib/io_streams/record/reader.rb +20 -16
- data/lib/io_streams/record/writer.rb +28 -28
- data/lib/io_streams/row/reader.rb +22 -26
- data/lib/io_streams/row/writer.rb +29 -28
- data/lib/io_streams/stream.rb +400 -0
- data/lib/io_streams/streams.rb +125 -0
- data/lib/io_streams/symmetric_encryption/reader.rb +5 -13
- data/lib/io_streams/symmetric_encryption/writer.rb +16 -15
- data/lib/io_streams/tabular/header.rb +9 -3
- data/lib/io_streams/tabular/parser/array.rb +8 -3
- data/lib/io_streams/tabular/parser/csv.rb +6 -2
- data/lib/io_streams/tabular/parser/hash.rb +4 -1
- data/lib/io_streams/tabular/parser/json.rb +3 -1
- data/lib/io_streams/tabular/parser/psv.rb +3 -1
- data/lib/io_streams/tabular/utility/csv_row.rb +9 -8
- data/lib/io_streams/utils.rb +22 -0
- data/lib/io_streams/version.rb +1 -1
- data/lib/io_streams/writer.rb +28 -0
- data/lib/io_streams/xlsx/reader.rb +7 -19
- data/lib/io_streams/zip/reader.rb +7 -26
- data/lib/io_streams/zip/writer.rb +21 -38
- data/lib/iostreams.rb +15 -15
- data/test/bzip2_reader_test.rb +3 -3
- data/test/bzip2_writer_test.rb +3 -3
- data/test/deprecated_test.rb +123 -0
- data/test/encode_reader_test.rb +3 -3
- data/test/encode_writer_test.rb +6 -6
- data/test/gzip_reader_test.rb +2 -2
- data/test/gzip_writer_test.rb +3 -3
- data/test/io_streams_test.rb +43 -136
- data/test/line_reader_test.rb +20 -20
- data/test/line_writer_test.rb +3 -3
- data/test/path_test.rb +30 -28
- data/test/paths/file_test.rb +206 -0
- data/test/paths/http_test.rb +34 -0
- data/test/paths/matcher_test.rb +111 -0
- data/test/paths/s3_test.rb +207 -0
- data/test/pgp_reader_test.rb +8 -8
- data/test/pgp_writer_test.rb +13 -13
- data/test/record_reader_test.rb +5 -5
- data/test/record_writer_test.rb +4 -4
- data/test/row_reader_test.rb +5 -5
- data/test/row_writer_test.rb +6 -6
- data/test/stream_test.rb +116 -0
- data/test/streams_test.rb +255 -0
- data/test/utils_test.rb +20 -0
- data/test/xlsx_reader_test.rb +3 -3
- data/test/zip_reader_test.rb +12 -12
- data/test/zip_writer_test.rb +5 -5
- metadata +33 -45
- data/lib/io_streams/base_path.rb +0 -72
- data/lib/io_streams/file/path.rb +0 -58
- data/lib/io_streams/file/reader.rb +0 -12
- data/lib/io_streams/file/writer.rb +0 -22
- data/lib/io_streams/http/reader.rb +0 -71
- data/lib/io_streams/s3.rb +0 -26
- data/lib/io_streams/s3/path.rb +0 -40
- data/lib/io_streams/s3/reader.rb +0 -28
- data/lib/io_streams/s3/writer.rb +0 -85
- data/lib/io_streams/sftp/reader.rb +0 -67
- data/lib/io_streams/sftp/writer.rb +0 -68
- data/test/base_path_test.rb +0 -35
- data/test/file_path_test.rb +0 -97
- data/test/file_reader_test.rb +0 -33
- data/test/file_writer_test.rb +0 -50
- data/test/http_reader_test.rb +0 -38
- data/test/s3_reader_test.rb +0 -41
- 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
|