iostreams 1.11.0 → 2.0.0
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/README.md +20 -2
- data/Rakefile +7 -0
- data/lib/io_streams/builder.rb +9 -9
- data/lib/io_streams/bzip2/writer.rb +1 -1
- data/lib/io_streams/encode/reader.rb +2 -2
- data/lib/io_streams/encode/writer.rb +5 -5
- data/lib/io_streams/gzip/reader.rb +1 -1
- data/lib/io_streams/gzip/writer.rb +1 -1
- data/lib/io_streams/io_streams.rb +45 -19
- data/lib/io_streams/line/reader.rb +2 -2
- data/lib/io_streams/line/writer.rb +1 -1
- data/lib/io_streams/path.rb +2 -2
- data/lib/io_streams/paths/file.rb +10 -10
- data/lib/io_streams/paths/http.rb +80 -7
- data/lib/io_streams/paths/matcher.rb +3 -3
- data/lib/io_streams/paths/s3.rb +3 -3
- data/lib/io_streams/paths/sftp.rb +7 -8
- data/lib/io_streams/pgp/reader.rb +23 -10
- data/lib/io_streams/pgp/writer.rb +93 -32
- data/lib/io_streams/pgp.rb +188 -60
- data/lib/io_streams/reader.rb +4 -4
- data/lib/io_streams/record/reader.rb +3 -4
- data/lib/io_streams/record/writer.rb +3 -4
- data/lib/io_streams/row/reader.rb +1 -1
- data/lib/io_streams/row/writer.rb +1 -1
- data/lib/io_streams/stream.rb +36 -30
- data/lib/io_streams/symmetric_encryption/reader.rb +2 -2
- data/lib/io_streams/symmetric_encryption/writer.rb +4 -4
- data/lib/io_streams/tabular/header.rb +18 -6
- data/lib/io_streams/tabular/parser/array.rb +0 -10
- data/lib/io_streams/tabular/parser/csv.rb +6 -38
- data/lib/io_streams/tabular/parser/fixed.rb +5 -5
- data/lib/io_streams/tabular/parser/psv.rb +0 -12
- data/lib/io_streams/tabular.rb +5 -10
- data/lib/io_streams/utils.rb +3 -2
- data/lib/io_streams/version.rb +1 -1
- data/lib/io_streams/writer.rb +6 -6
- data/lib/io_streams/xlsx/reader.rb +1 -1
- data/lib/io_streams/zip/writer.rb +22 -10
- data/lib/iostreams.rb +0 -1
- metadata +28 -111
- data/lib/io_streams/deprecated.rb +0 -216
- data/lib/io_streams/tabular/utility/csv_row.rb +0 -105
- data/test/builder_test.rb +0 -311
- data/test/bzip2_reader_test.rb +0 -27
- data/test/bzip2_writer_test.rb +0 -56
- data/test/deprecated_test.rb +0 -121
- data/test/encode_reader_test.rb +0 -51
- data/test/encode_writer_test.rb +0 -90
- data/test/files/embedded_lines_test.csv +0 -7
- data/test/files/multiple_files.zip +0 -0
- data/test/files/spreadsheet.xlsx +0 -0
- data/test/files/test.csv +0 -4
- data/test/files/test.json +0 -3
- data/test/files/test.psv +0 -4
- data/test/files/text file.txt +0 -3
- data/test/files/text.txt +0 -3
- data/test/files/text.txt.bz2 +0 -0
- data/test/files/text.txt.gz +0 -0
- data/test/files/text.txt.gz.zip +0 -0
- data/test/files/text.zip +0 -0
- data/test/files/text.zip.gz +0 -0
- data/test/files/unclosed_quote_large_test.csv +0 -1658
- data/test/files/unclosed_quote_test.csv +0 -4
- data/test/files/unclosed_quote_test2.csv +0 -3
- data/test/gzip_reader_test.rb +0 -27
- data/test/gzip_writer_test.rb +0 -52
- data/test/io_streams_test.rb +0 -132
- data/test/line_reader_test.rb +0 -325
- data/test/line_writer_test.rb +0 -59
- data/test/minimal_file_reader.rb +0 -25
- data/test/path_test.rb +0 -55
- data/test/paths/file_test.rb +0 -213
- data/test/paths/http_test.rb +0 -34
- data/test/paths/matcher_test.rb +0 -120
- data/test/paths/s3_test.rb +0 -220
- data/test/paths/sftp_test.rb +0 -106
- data/test/pgp_reader_test.rb +0 -46
- data/test/pgp_test.rb +0 -267
- data/test/pgp_writer_test.rb +0 -130
- data/test/record_reader_test.rb +0 -60
- data/test/record_writer_test.rb +0 -82
- data/test/row_reader_test.rb +0 -35
- data/test/row_writer_test.rb +0 -56
- data/test/stream_test.rb +0 -577
- data/test/tabular_test.rb +0 -338
- data/test/test_helper.rb +0 -40
- data/test/utils_test.rb +0 -20
- data/test/xlsx_reader_test.rb +0 -37
- data/test/zip_reader_test.rb +0 -53
- data/test/zip_writer_test.rb +0 -48
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
module IOStreams
|
|
2
|
-
UTF8_ENCODING = Encoding.find("UTF-8").freeze
|
|
3
|
-
BINARY_ENCODING = Encoding.find("BINARY").freeze
|
|
4
|
-
|
|
5
|
-
# Deprecated IOStreams from v0.x. Do not use, will be removed soon.
|
|
6
|
-
module Deprecated
|
|
7
|
-
def self.included(base)
|
|
8
|
-
base.extend ClassMethods
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
module ClassMethods
|
|
12
|
-
# DEPRECATED. Use `#path` or `#io`
|
|
13
|
-
# Examples:
|
|
14
|
-
# IOStreams.path("data.zip").reader { |f| f.read(100) }
|
|
15
|
-
#
|
|
16
|
-
# IOStreams.path(file_name).option(:encode, encoding: "BINARY").reader { |f| f.read(100) }
|
|
17
|
-
#
|
|
18
|
-
# io_stream = StringIO.new("Hello World")
|
|
19
|
-
# IOStreams.stream(io_stream).reader { |f| f.read(100) }
|
|
20
|
-
def reader(file_name_or_io, streams: nil, file_name: nil, encoding: nil, encode_cleaner: nil, encode_replace: nil, &block)
|
|
21
|
-
path = build_path(file_name_or_io, streams: streams, file_name: file_name, encoding: encoding, encode_cleaner: encode_cleaner, encode_replace: encode_replace)
|
|
22
|
-
path.reader(&block)
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
# DEPRECATED
|
|
26
|
-
def each_line(file_name_or_io, encoding: nil, encode_cleaner: nil, encode_replace: nil, **args, &block)
|
|
27
|
-
path = build_path(file_name_or_io, encoding: encoding, encode_cleaner: encode_cleaner, encode_replace: encode_replace)
|
|
28
|
-
path.each(:line, **args, &block)
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# DEPRECATED
|
|
32
|
-
def each_row(file_name_or_io, encoding: nil, encode_cleaner: nil, encode_replace: nil, **args, &block)
|
|
33
|
-
path = build_path(file_name_or_io, encoding: encoding, encode_cleaner: encode_cleaner, encode_replace: encode_replace)
|
|
34
|
-
path.each(:array, **args, &block)
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# DEPRECATED
|
|
38
|
-
def each_record(file_name_or_io, encoding: nil, encode_cleaner: nil, encode_replace: nil, **args, &block)
|
|
39
|
-
path = build_path(file_name_or_io, encoding: encoding, encode_cleaner: encode_cleaner, encode_replace: encode_replace)
|
|
40
|
-
path.each(:hash, **args, &block)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
# DEPRECATED. Use `#path` or `#io`
|
|
44
|
-
# Examples:
|
|
45
|
-
# IOStreams.path("data.zip").writer { |f| f.write("Hello World") }
|
|
46
|
-
#
|
|
47
|
-
# IOStreams.path(file_name).option(:encode, encoding: "BINARY").writer { |f| f.write("Hello World") }
|
|
48
|
-
#
|
|
49
|
-
# io_stream = StringIO.new("Hello World")
|
|
50
|
-
# IOStreams.stream(io_stream).writer { |f| f.write("Hello World") }
|
|
51
|
-
def writer(file_name_or_io, streams: nil, file_name: nil, encoding: nil, encode_cleaner: nil, encode_replace: nil, &block)
|
|
52
|
-
path = build_path(file_name_or_io, streams: streams, file_name: file_name, encoding: encoding, encode_cleaner: encode_cleaner, encode_replace: encode_replace)
|
|
53
|
-
path.writer(&block)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
# DEPRECATED
|
|
57
|
-
def line_writer(file_name_or_io, streams: nil, file_name: nil, encoding: nil, encode_cleaner: nil, encode_replace: nil, **args, &block)
|
|
58
|
-
path = build_path(file_name_or_io, streams: streams, file_name: file_name, encoding: encoding, encode_cleaner: encode_cleaner, encode_replace: encode_replace)
|
|
59
|
-
path.writer(:line, **args, &block)
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
# DEPRECATED
|
|
63
|
-
def row_writer(file_name_or_io, streams: nil, file_name: nil, encoding: nil, encode_cleaner: nil, encode_replace: nil, **args, &block)
|
|
64
|
-
path = build_path(file_name_or_io, streams: streams, file_name: file_name, encoding: encoding, encode_cleaner: encode_cleaner, encode_replace: encode_replace)
|
|
65
|
-
path.writer(:array, **args, &block)
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
# DEPRECATED
|
|
69
|
-
def record_writer(file_name_or_io, streams: nil, file_name: nil, encoding: nil, encode_cleaner: nil, encode_replace: nil, **args, &block)
|
|
70
|
-
path = build_path(file_name_or_io, streams: streams, file_name: file_name, encoding: encoding, encode_cleaner: encode_cleaner, encode_replace: encode_replace)
|
|
71
|
-
path.writer(:hash, **args, &block)
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
# Copies the source file/stream to the target file/stream.
|
|
75
|
-
# Returns [Integer] the number of bytes copied
|
|
76
|
-
#
|
|
77
|
-
# Example: Copy between 2 files
|
|
78
|
-
# IOStreams.copy('a.csv', 'b.csv')
|
|
79
|
-
#
|
|
80
|
-
# Example: Read content from a Xlsx file and write it out in CSV form.
|
|
81
|
-
# IOStreams.copy('a.xlsx', 'b.csv')
|
|
82
|
-
#
|
|
83
|
-
# Example:
|
|
84
|
-
# # Read content from a JSON file and write it out in CSV form.
|
|
85
|
-
# #
|
|
86
|
-
# # The output header for the CSV file is extracted from the first row in the JSON file.
|
|
87
|
-
# # If the first JSON row does not contain all the column names then they will be ignored
|
|
88
|
-
# # for the rest of the file.
|
|
89
|
-
# IOStreams.copy('a.json', 'b.csv')
|
|
90
|
-
#
|
|
91
|
-
# Example:
|
|
92
|
-
# # Read a PSV file and write out a CSV file from it.
|
|
93
|
-
# IOStreams.copy('a.psv', 'b.csv')
|
|
94
|
-
#
|
|
95
|
-
# Example:
|
|
96
|
-
# # Copy between 2 files, encrypting the target file with Symmetric Encryption
|
|
97
|
-
# # Since the target file_name already includes `.enc` in the filename, it is automatically
|
|
98
|
-
# # encrypted.
|
|
99
|
-
# IOStreams.copy('a.csv', 'b.csv.enc')
|
|
100
|
-
#
|
|
101
|
-
# Example:
|
|
102
|
-
# # Copy between 2 files, encrypting the target file with Symmetric Encryption
|
|
103
|
-
# # Since the target file_name does not include `.enc` in the filename, to encrypt it
|
|
104
|
-
# # the encryption stream is added.
|
|
105
|
-
# IOStreams.copy('a.csv', 'b', target_options: [:enc])
|
|
106
|
-
#
|
|
107
|
-
# Example:
|
|
108
|
-
# # Copy between 2 files, encrypting the target file with Symmetric Encryption
|
|
109
|
-
# # Since the target file_name does not include `.enc` in the filename, to encrypt it
|
|
110
|
-
# # the encryption stream is added, along with the optional compress option.
|
|
111
|
-
# IOStreams.copy('a.csv', 'b', target_options: [enc: { compress: true }])
|
|
112
|
-
#
|
|
113
|
-
# Example:
|
|
114
|
-
# # Create a pgp encrypted file.
|
|
115
|
-
# # For PGP Encryption the recipients email address is required.
|
|
116
|
-
# IOStreams.copy('a.xlsx', 'b.csv.pgp', target_options: [:csv, pgp: { recipient_email: 'user@nospam.org' }])
|
|
117
|
-
#
|
|
118
|
-
# Example: Copy between 2 existing streams
|
|
119
|
-
# IOStreams.reader('a.csv') do |source_stream|
|
|
120
|
-
# IOStreams.writer('b.csv.enc') do |target_stream|
|
|
121
|
-
# IOStreams.copy(source_stream, target_stream)
|
|
122
|
-
# end
|
|
123
|
-
# end
|
|
124
|
-
#
|
|
125
|
-
# Example:
|
|
126
|
-
# # Copy between 2 csv files, reducing the number of columns present and encrypting the
|
|
127
|
-
# # target file with Symmetric Encryption
|
|
128
|
-
# output_headers = %w[name address]
|
|
129
|
-
# IOStreams.copy(
|
|
130
|
-
# 'a.csv',
|
|
131
|
-
# 'b.csv.enc',
|
|
132
|
-
# target_options: [csv:{headers: output_headers}, enc: {compress: true}]
|
|
133
|
-
# )
|
|
134
|
-
#
|
|
135
|
-
# Example:
|
|
136
|
-
# # Copy a locally encrypted file to AWS S3.
|
|
137
|
-
# # Decrypts the file, then compresses it with gzip as it is being streamed into S3.
|
|
138
|
-
# # Useful for when the entire bucket is encrypted on S3.
|
|
139
|
-
# IOStreams.copy('a.csv.enc', 's3://my_bucket/b.csv.gz')
|
|
140
|
-
def copy(source_file_name_or_io, target_file_name_or_io, buffer_size: nil, source_options: {}, target_options: {})
|
|
141
|
-
# TODO: prevent stream conversions when reader and writer streams are the same!
|
|
142
|
-
reader(source_file_name_or_io, **source_options) do |source_stream|
|
|
143
|
-
writer(target_file_name_or_io, **target_options) do |target_stream|
|
|
144
|
-
IO.copy_stream(source_stream, target_stream)
|
|
145
|
-
end
|
|
146
|
-
end
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
# DEPRECATED
|
|
150
|
-
def reader_stream?(file_name_or_io)
|
|
151
|
-
file_name_or_io.respond_to?(:read)
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
# DEPRECATED
|
|
155
|
-
def writer_stream?(file_name_or_io)
|
|
156
|
-
file_name_or_io.respond_to?(:write)
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
# DEPRECATED. Use Path#compressed?
|
|
160
|
-
def compressed?(file_name)
|
|
161
|
-
Path.new(file_name).compressed?
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
# DEPRECATED. Use Path#encrypted?
|
|
165
|
-
def encrypted?(file_name)
|
|
166
|
-
Path.new(file_name).encrypted?
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
# DEPRECATED
|
|
170
|
-
def line_reader(file_name_or_io, streams: nil, file_name: nil, encoding: nil, encode_cleaner: nil, encode_replace: nil, **args, &block)
|
|
171
|
-
path = build_path(file_name_or_io, streams: streams, file_name: file_name, encoding: encoding, encode_cleaner: encode_cleaner, encode_replace: encode_replace)
|
|
172
|
-
path.reader(:line, **args, &block)
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
# DEPRECATED
|
|
176
|
-
def row_reader(file_name_or_io, streams: nil, file_name: nil, encoding: nil, encode_cleaner: nil, encode_replace: nil, **args, &block)
|
|
177
|
-
path = build_path(file_name_or_io, streams: streams, file_name: file_name, encoding: encoding, encode_cleaner: encode_cleaner, encode_replace: encode_replace)
|
|
178
|
-
path.reader(:line, **args, &block)
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
# DEPRECATED
|
|
182
|
-
def record_reader(file_name_or_io, streams: nil, file_name: nil, encoding: nil, encode_cleaner: nil, encode_replace: nil, **args, &block)
|
|
183
|
-
path = build_path(file_name_or_io, streams: streams, file_name: file_name, encoding: encoding, encode_cleaner: encode_cleaner, encode_replace: encode_replace)
|
|
184
|
-
path.reader(:hash, **args, &block)
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
private
|
|
188
|
-
|
|
189
|
-
def build_path(file_name_or_io, streams: nil, file_name: nil, encoding: nil, encode_cleaner: nil, encode_replace: nil)
|
|
190
|
-
path = new(file_name_or_io)
|
|
191
|
-
path.file_name(file_name) if file_name
|
|
192
|
-
|
|
193
|
-
apply_old_style_streams(path, streams) if streams
|
|
194
|
-
|
|
195
|
-
if encoding || encode_cleaner || encode_replace
|
|
196
|
-
path.option_or_stream(:encode, encoding: encoding, cleaner: encode_cleaner, replace: encode_replace)
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
path
|
|
200
|
-
end
|
|
201
|
-
|
|
202
|
-
# Applies old form streams to the path
|
|
203
|
-
def apply_old_style_streams(path, streams)
|
|
204
|
-
if streams.is_a?(Symbol)
|
|
205
|
-
path.stream(streams)
|
|
206
|
-
elsif streams.is_a?(Array)
|
|
207
|
-
streams.each { |stream| apply_old_style_streams(path, stream) }
|
|
208
|
-
elsif streams.is_a?(Hash)
|
|
209
|
-
streams.each_pair { |stream, options| path.stream(stream, **options) }
|
|
210
|
-
else
|
|
211
|
-
raise ArgumentError, "Invalid old style stream supplied: #{params.inspect}"
|
|
212
|
-
end
|
|
213
|
-
end
|
|
214
|
-
end
|
|
215
|
-
end
|
|
216
|
-
end
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
require "csv"
|
|
2
|
-
module IOStreams
|
|
3
|
-
class Tabular
|
|
4
|
-
module Utility
|
|
5
|
-
# For parsing a single line of CSV at a time
|
|
6
|
-
# 2 to 3 times better performance than CSV.parse_line and considerably less
|
|
7
|
-
# garbage collection required.
|
|
8
|
-
#
|
|
9
|
-
# Note: Only used prior to Ruby 2.6
|
|
10
|
-
class CSVRow < ::CSV
|
|
11
|
-
UTF8_ENCODING = Encoding.find("UTF-8").freeze
|
|
12
|
-
|
|
13
|
-
def initialize(encoding = UTF8_ENCODING)
|
|
14
|
-
@io = StringIO.new("".force_encoding(encoding))
|
|
15
|
-
super(@io, row_sep: "")
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
# Parse a single line of CSV data
|
|
19
|
-
# Parameters
|
|
20
|
-
# line [String]
|
|
21
|
-
# A single line of CSV data without any line terminators
|
|
22
|
-
def parse(line)
|
|
23
|
-
return if IOStreams::Utils.blank?(line)
|
|
24
|
-
return if @skip_lines&.match(line)
|
|
25
|
-
|
|
26
|
-
in_extended_col = false
|
|
27
|
-
csv = []
|
|
28
|
-
parts = line.split(@col_sep, -1)
|
|
29
|
-
csv << nil if parts.empty?
|
|
30
|
-
|
|
31
|
-
# This loop is the hot path of csv parsing. Some things may be non-dry
|
|
32
|
-
# for a reason. Make sure to benchmark when refactoring.
|
|
33
|
-
parts.each do |part|
|
|
34
|
-
if in_extended_col
|
|
35
|
-
# If we are continuing a previous column
|
|
36
|
-
if part[-1] == @quote_char && part.count(@quote_char).odd?
|
|
37
|
-
# extended column ends
|
|
38
|
-
csv.last << part[0..-2]
|
|
39
|
-
raise MalformedCSVError, "Missing or stray quote in line #{lineno + 1}" if csv.last =~ @parsers[:stray_quote]
|
|
40
|
-
|
|
41
|
-
csv.last.gsub!(@quote_char * 2, @quote_char)
|
|
42
|
-
in_extended_col = false
|
|
43
|
-
else
|
|
44
|
-
csv.last << part
|
|
45
|
-
csv.last << @col_sep
|
|
46
|
-
end
|
|
47
|
-
elsif part[0] == @quote_char
|
|
48
|
-
# If we are starting a new quoted column
|
|
49
|
-
if part[-1] != @quote_char || part.count(@quote_char).odd?
|
|
50
|
-
# start an extended column
|
|
51
|
-
csv << part[1..-1]
|
|
52
|
-
csv.last << @col_sep
|
|
53
|
-
in_extended_col = true
|
|
54
|
-
else
|
|
55
|
-
# regular quoted column
|
|
56
|
-
csv << part[1..-2]
|
|
57
|
-
raise MalformedCSVError, "Missing or stray quote in line #{lineno + 1}" if csv.last =~ @parsers[:stray_quote]
|
|
58
|
-
|
|
59
|
-
csv.last.gsub!(@quote_char * 2, @quote_char)
|
|
60
|
-
end
|
|
61
|
-
elsif part =~ @parsers[:quote_or_nl]
|
|
62
|
-
# Unquoted field with bad characters.
|
|
63
|
-
if part =~ @parsers[:nl_or_lf]
|
|
64
|
-
raise MalformedCSVError, "Unquoted fields do not allow \\r or \\n (line #{lineno + 1})."
|
|
65
|
-
else
|
|
66
|
-
raise MalformedCSVError, "Illegal quoting in line #{lineno + 1}."
|
|
67
|
-
end
|
|
68
|
-
else
|
|
69
|
-
# Regular ole unquoted field.
|
|
70
|
-
csv << (part.empty? ? nil : part)
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
# Replace tacked on @col_sep with @row_sep if we are still in an extended
|
|
75
|
-
# column.
|
|
76
|
-
csv[-1][-1] = @row_sep if in_extended_col
|
|
77
|
-
|
|
78
|
-
raise MalformedCSVError, "Unclosed quoted field on line #{lineno + 1}." if in_extended_col
|
|
79
|
-
|
|
80
|
-
@lineno += 1
|
|
81
|
-
|
|
82
|
-
# save fields unconverted fields, if needed...
|
|
83
|
-
unconverted = csv.dup if @unconverted_fields
|
|
84
|
-
|
|
85
|
-
# convert fields, if needed...
|
|
86
|
-
csv = convert_fields(csv) unless @use_headers || @converters.empty?
|
|
87
|
-
# parse out header rows and handle CSV::Row conversions...
|
|
88
|
-
csv = parse_headers(csv) if @use_headers
|
|
89
|
-
|
|
90
|
-
# inject unconverted fields and accessor, if requested...
|
|
91
|
-
add_unconverted_fields(csv, unconverted) if @unconverted_fields && (!csv.respond_to? :unconverted_fields)
|
|
92
|
-
|
|
93
|
-
csv
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
# Return the supplied array as a single line CSV string.
|
|
97
|
-
def render(row)
|
|
98
|
-
row.map(&@quote).join(@col_sep) + @row_sep # quote and separate
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
alias to_csv render
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
end
|
data/test/builder_test.rb
DELETED
|
@@ -1,311 +0,0 @@
|
|
|
1
|
-
require_relative "test_helper"
|
|
2
|
-
|
|
3
|
-
class BuilderTest < Minitest::Test
|
|
4
|
-
describe IOStreams::Builder do
|
|
5
|
-
let(:file_name) { "my/path/abc.bcd.xlsx.zip.gz.pgp" }
|
|
6
|
-
let(:streams) { IOStreams::Builder.new(file_name) }
|
|
7
|
-
|
|
8
|
-
describe "#option" do
|
|
9
|
-
it "adds one option" do
|
|
10
|
-
streams.option(:pgp, passphrase: "unlock-me")
|
|
11
|
-
assert_equal({pgp: {passphrase: "unlock-me"}}, streams.options)
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
it "adds options in order" do
|
|
15
|
-
streams.option(:pgp, passphrase: "unlock-me")
|
|
16
|
-
streams.option(:enc, compress: false)
|
|
17
|
-
assert_equal({pgp: {passphrase: "unlock-me"}, enc: {compress: false}}, streams.options)
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
it "will not add an option if a stream was already set" do
|
|
21
|
-
streams.stream(:pgp, passphrase: "unlock-me")
|
|
22
|
-
assert_raises ArgumentError do
|
|
23
|
-
streams.option(:pgp, passphrase: "unlock-me")
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
it "will not add an invalid option" do
|
|
28
|
-
assert_raises ArgumentError do
|
|
29
|
-
streams.option(:blah, value: 23)
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
describe "with no file_name" do
|
|
34
|
-
let(:file_name) { nil }
|
|
35
|
-
|
|
36
|
-
it "prevents options being set" do
|
|
37
|
-
assert_raises ArgumentError do
|
|
38
|
-
streams.option(:pgp, passphrase: "unlock-me")
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
describe "#format" do
|
|
45
|
-
it "detects the format from the file name" do
|
|
46
|
-
streams = IOStreams::Builder.new("abc.json")
|
|
47
|
-
assert_equal :json, streams.format
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
it "is nil if the file name has no meaningful format" do
|
|
51
|
-
assert_nil streams.format
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
it "returns set format with no file_name" do
|
|
55
|
-
streams = IOStreams::Builder.new
|
|
56
|
-
streams.format = :csv
|
|
57
|
-
assert_equal :csv, streams.format
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
it "returns set format with file_name" do
|
|
61
|
-
streams = IOStreams::Builder.new("abc.json")
|
|
62
|
-
streams.format = :csv
|
|
63
|
-
assert_equal :csv, streams.format
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
it "validates bad format" do
|
|
67
|
-
assert_raises ArgumentError do
|
|
68
|
-
streams.format = :blah
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
describe "#stream" do
|
|
74
|
-
it "adds one stream" do
|
|
75
|
-
streams.stream(:pgp, passphrase: "unlock-me")
|
|
76
|
-
assert_equal({pgp: {passphrase: "unlock-me"}}, streams.streams)
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
it "adds streams in order" do
|
|
80
|
-
streams.stream(:pgp, passphrase: "unlock-me")
|
|
81
|
-
streams.stream(:enc, compress: false)
|
|
82
|
-
assert_equal({pgp: {passphrase: "unlock-me"}, enc: {compress: false}}, streams.streams)
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
it "will not add a stream if an option was already set" do
|
|
86
|
-
streams.option(:pgp, passphrase: "unlock-me")
|
|
87
|
-
assert_raises ArgumentError do
|
|
88
|
-
streams.stream(:pgp, passphrase: "unlock-me")
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
it "will not add an invalid stream" do
|
|
93
|
-
assert_raises ArgumentError do
|
|
94
|
-
streams.stream(:blah, value: 23)
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
describe "#reader" do
|
|
100
|
-
let :gzip_string do
|
|
101
|
-
io_string = StringIO.new("".b)
|
|
102
|
-
IOStreams::Gzip::Writer.stream(io_string) do |io|
|
|
103
|
-
io.write("Hello World")
|
|
104
|
-
end
|
|
105
|
-
io_string.string
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
it "directly calls block for an empty stream" do
|
|
109
|
-
string_io = StringIO.new
|
|
110
|
-
value = nil
|
|
111
|
-
streams.stream(:none)
|
|
112
|
-
streams.reader(string_io) do |io|
|
|
113
|
-
assert_equal io, string_io
|
|
114
|
-
value = 32
|
|
115
|
-
end
|
|
116
|
-
assert_equal 32, value
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
it "returns the reader" do
|
|
120
|
-
string_io = StringIO.new(gzip_string)
|
|
121
|
-
streams.stream(:gz)
|
|
122
|
-
streams.reader(string_io) do |io|
|
|
123
|
-
assert io.is_a?(::Zlib::GzipReader), io
|
|
124
|
-
end
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
it "returns the last reader" do
|
|
128
|
-
string_io = StringIO.new(gzip_string)
|
|
129
|
-
streams.stream(:encode)
|
|
130
|
-
streams.stream(:gz)
|
|
131
|
-
streams.reader(string_io) do |io|
|
|
132
|
-
assert io.is_a?(IOStreams::Encode::Reader), io
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
describe "#writer" do
|
|
138
|
-
it "directly calls block for an empty stream" do
|
|
139
|
-
string_io = StringIO.new
|
|
140
|
-
value = nil
|
|
141
|
-
streams.stream(:none)
|
|
142
|
-
streams.writer(string_io) do |io|
|
|
143
|
-
assert_equal io, string_io
|
|
144
|
-
value = 32
|
|
145
|
-
end
|
|
146
|
-
assert_equal 32, value
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
it "returns the reader" do
|
|
150
|
-
string_io = StringIO.new
|
|
151
|
-
streams.stream(:zip)
|
|
152
|
-
streams.writer(string_io) do |io|
|
|
153
|
-
assert io.is_a?(ZipTricks::Streamer::Writable), io
|
|
154
|
-
end
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
it "returns the last reader" do
|
|
158
|
-
string_io = StringIO.new
|
|
159
|
-
streams.stream(:encode)
|
|
160
|
-
streams.stream(:zip)
|
|
161
|
-
streams.writer(string_io) do |io|
|
|
162
|
-
assert io.is_a?(IOStreams::Encode::Writer), io
|
|
163
|
-
end
|
|
164
|
-
end
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
# Internal methods
|
|
168
|
-
|
|
169
|
-
describe "#class_for_stream" do
|
|
170
|
-
it "xlsx" do
|
|
171
|
-
assert_equal IOStreams::Xlsx::Reader, streams.send(:class_for_stream, :reader, :xlsx)
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
it "gzip" do
|
|
175
|
-
assert_equal IOStreams::Gzip::Writer, streams.send(:class_for_stream, :writer, :gzip)
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
it "unknown" do
|
|
179
|
-
assert_raises ArgumentError do
|
|
180
|
-
streams.send(:class_for_stream, :reader, :unknown)
|
|
181
|
-
end
|
|
182
|
-
end
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
describe "#parse_extensions" do
|
|
186
|
-
it "single stream" do
|
|
187
|
-
streams = IOStreams::Builder.new("my/path/abc.xlsx")
|
|
188
|
-
assert_equal %i[xlsx], streams.send(:parse_extensions)
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
it "empty" do
|
|
192
|
-
streams = IOStreams::Builder.new("my/path/abc.csv")
|
|
193
|
-
assert_equal [], streams.send(:parse_extensions)
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
it "handles multiple extensions" do
|
|
197
|
-
assert_equal %i[xlsx zip gz pgp], streams.send(:parse_extensions)
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
describe "case-insensitive" do
|
|
201
|
-
let(:file_name) { "a.XlsX.GzIp" }
|
|
202
|
-
|
|
203
|
-
it "is case-insensitive" do
|
|
204
|
-
assert_equal %i[xlsx gzip], streams.send(:parse_extensions)
|
|
205
|
-
end
|
|
206
|
-
end
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
describe "#pipeline" do
|
|
210
|
-
it "with stream and file name" do
|
|
211
|
-
expected = {enc: {compress: false}}
|
|
212
|
-
streams.stream(:enc, compress: false)
|
|
213
|
-
assert_equal expected, streams.pipeline
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
it "no file name, streams, or options" do
|
|
217
|
-
expected = {}
|
|
218
|
-
streams = IOStreams::Builder.new
|
|
219
|
-
assert_equal expected, streams.pipeline
|
|
220
|
-
end
|
|
221
|
-
|
|
222
|
-
it "file name without options" do
|
|
223
|
-
expected = {xlsx: {}, zip: {}, gz: {}, pgp: {}}
|
|
224
|
-
assert_equal expected, streams.pipeline
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
it "file name with encode option" do
|
|
228
|
-
expected = {encode: {encoding: "BINARY"}, xlsx: {}, zip: {}, gz: {}, pgp: {}}
|
|
229
|
-
streams.option(:encode, encoding: "BINARY")
|
|
230
|
-
assert_equal expected, streams.pipeline
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
it "file name with option" do
|
|
234
|
-
expected = {xlsx: {}, zip: {}, gz: {}, pgp: {passphrase: "unlock-me"}}
|
|
235
|
-
streams.option(:pgp, passphrase: "unlock-me")
|
|
236
|
-
assert_equal expected, streams.pipeline
|
|
237
|
-
end
|
|
238
|
-
end
|
|
239
|
-
|
|
240
|
-
describe "#remove_from_pipeline" do
|
|
241
|
-
let(:file_name) { "my/path/abc.bz2.pgp" }
|
|
242
|
-
it "removes a named stream from the pipeline" do
|
|
243
|
-
assert_equal({bz2: {}, pgp: {}}, streams.pipeline)
|
|
244
|
-
streams.remove_from_pipeline(:bz2)
|
|
245
|
-
assert_equal({pgp: {}}, streams.pipeline)
|
|
246
|
-
end
|
|
247
|
-
it "removes a named stream from the pipeline with options" do
|
|
248
|
-
streams.option(:pgp, passphrase: "unlock-me")
|
|
249
|
-
assert_equal({bz2: {}, pgp: {passphrase: "unlock-me"}}, streams.pipeline)
|
|
250
|
-
streams.remove_from_pipeline(:bz2)
|
|
251
|
-
assert_equal({pgp: {passphrase: "unlock-me"}}, streams.pipeline)
|
|
252
|
-
end
|
|
253
|
-
end
|
|
254
|
-
|
|
255
|
-
describe "#execute" do
|
|
256
|
-
it "directly calls block for an empty stream" do
|
|
257
|
-
string_io = StringIO.new
|
|
258
|
-
value = nil
|
|
259
|
-
streams.send(:execute, :writer, {}, string_io) do |io|
|
|
260
|
-
assert_equal io, string_io
|
|
261
|
-
value = 32
|
|
262
|
-
end
|
|
263
|
-
assert_equal 32, value
|
|
264
|
-
end
|
|
265
|
-
|
|
266
|
-
it "calls last block in one element stream" do
|
|
267
|
-
pipeline = {simple: {arg: "first"}}
|
|
268
|
-
string_io = StringIO.new
|
|
269
|
-
streams.send(:execute, :writer, pipeline, string_io) { |io| io.write("last") }
|
|
270
|
-
assert_equal "first>last", string_io.string
|
|
271
|
-
end
|
|
272
|
-
|
|
273
|
-
it "chains blocks in 2 element stream" do
|
|
274
|
-
pipeline = {simple: {arg: "first"}, simple2: {arg: "second"}}
|
|
275
|
-
string_io = StringIO.new
|
|
276
|
-
streams.send(:execute, :writer, pipeline, string_io) { |io| io.write("last") }
|
|
277
|
-
assert_equal "second>first>last", string_io.string
|
|
278
|
-
end
|
|
279
|
-
|
|
280
|
-
it "chains blocks in 3 element stream" do
|
|
281
|
-
pipeline = {simple: {arg: "first"}, simple2: {arg: "second"}, simple3: {arg: "third"}}
|
|
282
|
-
string_io = StringIO.new
|
|
283
|
-
streams.send(:execute, :writer, pipeline, string_io) { |io| io.write("last") }
|
|
284
|
-
assert_equal "third>second>first>last", string_io.string
|
|
285
|
-
end
|
|
286
|
-
end
|
|
287
|
-
|
|
288
|
-
class SimpleStream
|
|
289
|
-
def self.stream(io, **args)
|
|
290
|
-
yield new(io, **args)
|
|
291
|
-
end
|
|
292
|
-
|
|
293
|
-
def self.open(file_name_or_io, **args, &block)
|
|
294
|
-
file_name_or_io.is_a?(String) ? file(file_name_or_io, **args, &block) : stream(file_name_or_io, **args, &block)
|
|
295
|
-
end
|
|
296
|
-
|
|
297
|
-
def initialize(io, arg:)
|
|
298
|
-
@io = io
|
|
299
|
-
@arg = arg
|
|
300
|
-
end
|
|
301
|
-
|
|
302
|
-
def write(data)
|
|
303
|
-
@io.write("#{@arg}>#{data}")
|
|
304
|
-
end
|
|
305
|
-
end
|
|
306
|
-
|
|
307
|
-
IOStreams.register_extension(:simple, nil, SimpleStream)
|
|
308
|
-
IOStreams.register_extension(:simple2, nil, SimpleStream)
|
|
309
|
-
IOStreams.register_extension(:simple3, nil, SimpleStream)
|
|
310
|
-
end
|
|
311
|
-
end
|
data/test/bzip2_reader_test.rb
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
require_relative "test_helper"
|
|
2
|
-
|
|
3
|
-
class Bzip2ReaderTest < Minitest::Test
|
|
4
|
-
describe IOStreams::Bzip2::Reader do
|
|
5
|
-
let :file_name do
|
|
6
|
-
File.join(File.dirname(__FILE__), "files", "text.txt.bz2")
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
let :decompressed do
|
|
10
|
-
File.read(File.join(File.dirname(__FILE__), "files", "text.txt"))
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
describe ".file" do
|
|
14
|
-
it "file" do
|
|
15
|
-
result = IOStreams::Bzip2::Reader.file(file_name, &:read)
|
|
16
|
-
assert_equal decompressed, result
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
it "stream" do
|
|
20
|
-
result = File.open(file_name) do |file|
|
|
21
|
-
IOStreams::Bzip2::Reader.stream(file, &:read)
|
|
22
|
-
end
|
|
23
|
-
assert_equal decompressed, result
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|