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.
- 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
|