iostreams 2.1.0 → 2.2.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/docs/config.md +13 -9
- data/docs/copy_files.md +20 -2
- data/docs/extensions.md +18 -4
- data/docs/formats.md +644 -16
- data/docs/path.md +18 -2
- data/docs/streams.md +54 -9
- data/docs/tutorial.md +14 -2
- data/docs/upgrading.md +470 -23
- data/lib/io_streams/builder/deprecations.rb +250 -0
- data/lib/io_streams/builder.rb +256 -39
- data/lib/io_streams/bzip2/reader.rb +18 -2
- data/lib/io_streams/bzip2/writer.rb +11 -2
- data/lib/io_streams/deprecation/methods.rb +51 -0
- data/lib/io_streams/deprecation/once.rb +27 -0
- data/lib/io_streams/deprecation/option_names.rb +26 -0
- data/lib/io_streams/deprecation/words.rb +42 -0
- data/lib/io_streams/deprecation.rb +259 -0
- data/lib/io_streams/encode/reader.rb +54 -3
- data/lib/io_streams/encode/writer.rb +19 -2
- data/lib/io_streams/encode.rb +99 -0
- data/lib/io_streams/errors.rb +51 -0
- data/lib/io_streams/gzip/reader.rb +11 -0
- data/lib/io_streams/io_streams.rb +365 -35
- data/lib/io_streams/line/reader.rb +2 -1
- data/lib/io_streams/line/utf8_reader.rb +23 -0
- data/lib/io_streams/path.rb +125 -35
- data/lib/io_streams/paths/file.rb +72 -9
- data/lib/io_streams/paths/http.rb +48 -2
- data/lib/io_streams/paths/matcher.rb +6 -3
- data/lib/io_streams/paths/s3.rb +180 -9
- data/lib/io_streams/paths/sftp.rb +47 -3
- data/lib/io_streams/paths.rb +23 -0
- data/lib/io_streams/pgp/reader.rb +18 -0
- data/lib/io_streams/pgp/writer.rb +16 -0
- data/lib/io_streams/pgp.rb +16 -2
- data/lib/io_streams/reader.rb +44 -0
- data/lib/io_streams/record/reader.rb +59 -68
- data/lib/io_streams/record/writer.rb +17 -10
- data/lib/io_streams/row/header_row.rb +21 -0
- data/lib/io_streams/row/reader.rb +52 -15
- data/lib/io_streams/row/writer.rb +64 -13
- data/lib/io_streams/schema/columns.rb +456 -0
- data/lib/io_streams/schema/constraints.rb +99 -0
- data/lib/io_streams/schema/field.rb +967 -0
- data/lib/io_streams/schema/json_number.rb +56 -0
- data/lib/io_streams/schema/keys.rb +78 -0
- data/lib/io_streams/schema/pattern.rb +257 -0
- data/lib/io_streams/schema/source.rb +31 -0
- data/lib/io_streams/schema.rb +616 -0
- data/lib/io_streams/stream.rb +836 -67
- data/lib/io_streams/symmetric_encryption/reader.rb +8 -0
- data/lib/io_streams/tabular/header.rb +29 -0
- data/lib/io_streams/tabular/header_mapping.rb +332 -0
- data/lib/io_streams/tabular/mapping.rb +165 -0
- data/lib/io_streams/tabular/parser/array.rb +5 -0
- data/lib/io_streams/tabular/parser/base.rb +24 -0
- data/lib/io_streams/tabular/parser/fixed.rb +197 -0
- data/lib/io_streams/tabular/parser/fixed_fields.rb +160 -0
- data/lib/io_streams/tabular/parser/hash.rb +10 -0
- data/lib/io_streams/tabular/parser/json.rb +17 -0
- data/lib/io_streams/tabular/parser/json_fields.rb +132 -0
- data/lib/io_streams/tabular/schema_mapping.rb +361 -0
- data/lib/io_streams/tabular.rb +344 -30
- data/lib/io_streams/utils.rb +441 -6
- data/lib/io_streams/version.rb +1 -1
- data/lib/io_streams/writer.rb +27 -0
- data/lib/io_streams/xlsx/cells.rb +127 -0
- data/lib/io_streams/xlsx/reader.rb +56 -0
- data/lib/io_streams/xlsx/schema_reader.rb +90 -0
- data/lib/io_streams/xlsx/v3_writer.rb +16 -0
- data/lib/io_streams/zip/reader.rb +7 -0
- data/lib/io_streams/zip/writer.rb +24 -2
- data/lib/iostreams.rb +21 -17
- metadata +28 -2
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
module IOStreams
|
|
2
|
+
class Builder
|
|
3
|
+
# What v2.2 reports about the streams, options and format of a builder as deprecation warnings: those that
|
|
4
|
+
# IOStreams v3.0 removes or rejects, and those whose results change in v3.0, with what to use instead. It is part
|
|
5
|
+
# of the Builder, which includes it, since the Builder owns the settings that it reports on.
|
|
6
|
+
module Deprecations
|
|
7
|
+
# A format set with #format= that is not the format of the text that the streams read, see #text_format, such as
|
|
8
|
+
# `:psv` for a spreadsheet, whose rows are read as CSV, is deprecated, since v3.0 raises ArgumentError when its
|
|
9
|
+
# lines, rows or records are read, rather than read them in that format. Reports a deprecation warning when they are
|
|
10
|
+
# read, as v3.0 raises then, whichever way the streams were set.
|
|
11
|
+
#
|
|
12
|
+
# So is reading them in the format that the file name names, such as `:psv` for `report.psv.xlsx`, without a format
|
|
13
|
+
# set, since v3.0 reads them in the format of the text whatever the file name, so that `format(:csv)` reads them
|
|
14
|
+
# the same in both versions.
|
|
15
|
+
#
|
|
16
|
+
# Neither is looked for while deprecation warnings are off, see `IOStreams::Deprecation.on?`, since finding the
|
|
17
|
+
# format of the text would build the pipeline once more for each read.
|
|
18
|
+
def warn_text_format
|
|
19
|
+
return unless Deprecation.on?
|
|
20
|
+
|
|
21
|
+
format = self.format
|
|
22
|
+
return if format.nil?
|
|
23
|
+
|
|
24
|
+
text_format = self.text_format
|
|
25
|
+
return if text_format.nil? || format == text_format
|
|
26
|
+
|
|
27
|
+
if @set_format
|
|
28
|
+
Deprecation.warn("format(#{format.inspect}) is deprecated when the streams read the rows as " \
|
|
29
|
+
"#{text_format.inspect}, such as those of a spreadsheet, since v3.0 raises ArgumentError for " \
|
|
30
|
+
"it, set format(#{text_format.inspect}) instead, or no format")
|
|
31
|
+
else
|
|
32
|
+
Deprecation.warn("the format #{format.inspect} that the file name names is deprecated when the streams read " \
|
|
33
|
+
"the rows as #{text_format.inspect}, such as those of a spreadsheet, since v3.0 reads them as " \
|
|
34
|
+
"#{text_format.inspect} whatever the file name, set format(#{text_format.inspect}) to read " \
|
|
35
|
+
"them so in both versions")
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# An encoding set with #encoding is deprecated when the rows and records of a spreadsheet are read, see
|
|
40
|
+
# `IOStreams::Stream#warn_cell_rows`, which are its cells rather than lines of text, since v3.0 raises ArgumentError
|
|
41
|
+
# for it then. Its lines still take it. Without it, a schema reads the text of the cells as UTF-8, see #utf8_text?,
|
|
42
|
+
# as v3.0 does.
|
|
43
|
+
def warn_cells_encoding
|
|
44
|
+
return if @encoding.nil?
|
|
45
|
+
|
|
46
|
+
Deprecation.warn("#encoding is deprecated when the rows and records of a spreadsheet are read, which are its " \
|
|
47
|
+
"cells rather than lines of text, since v3.0 raises ArgumentError for it, so remove it, and " \
|
|
48
|
+
"give its columns a schema, with which v2.2 reads the text of its cells as UTF-8; " \
|
|
49
|
+
"each(:line) still takes it")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Returns [Array(Array<Symbol>, String, Hash|nil)|nil] the methods that the stream that the application's block
|
|
53
|
+
# receives in v3.0 answers, its name for a deprecation warning, such as "the :gz stream", and what replaces each
|
|
54
|
+
# method that v3.0 removes, when the stream says, see `IOStreams::Reader.v3_stream_advice`, when the stream that it
|
|
55
|
+
# receives in v2.2 answers others, otherwise nil. The stream closest to the application decides, with its options,
|
|
56
|
+
# see `IOStreams::Reader.v3_stream_methods`. Without any streams, the block receives the IO that they would read or
|
|
57
|
+
# write, which the supplied source describes, see `IOStreams::Stream#v3_source` and `IOStreams::Stream#v3_target`.
|
|
58
|
+
#
|
|
59
|
+
# Parameters:
|
|
60
|
+
# type: [:reader|:writer]
|
|
61
|
+
# source: [Array(Array<Symbol>, String)|nil]
|
|
62
|
+
# The methods and the name of the IO that the streams read or write in v3.0, when it differs from the one in
|
|
63
|
+
# v2.2, otherwise nil.
|
|
64
|
+
#
|
|
65
|
+
# Yields, only when the stream closest to the application asks and is the only stream, to ask whether the streams
|
|
66
|
+
# read a regular local file in v3.0. The block answers true or false.
|
|
67
|
+
#
|
|
68
|
+
# Asked before the streams are opened, so it raises nothing, not even for a stream without a class of the type, which
|
|
69
|
+
# raises as before once it is opened.
|
|
70
|
+
def v3_stream(type, source)
|
|
71
|
+
streams = pipeline
|
|
72
|
+
name, options = streams.first
|
|
73
|
+
return source if name.nil?
|
|
74
|
+
|
|
75
|
+
klass = IOStreams.extensions[name]&.public_send(:"#{type}_class")
|
|
76
|
+
return unless klass.respond_to?(:v3_stream_methods)
|
|
77
|
+
|
|
78
|
+
methods = klass.v3_stream_methods(options) { streams.size == 1 && yield }
|
|
79
|
+
return unless methods
|
|
80
|
+
|
|
81
|
+
[methods, "the #{name.inspect} stream", (klass.v3_stream_advice if klass.respond_to?(:v3_stream_advice))]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Returns [Symbol|nil] the format that the rows and records of this file are read and written in, when IOStreams
|
|
85
|
+
# v3.0 reads and writes them in its `:text` format instead, as the text of each line: when no format was set, so
|
|
86
|
+
# that it is taken from a file name whose format extension is `.text`, see `IOStreams::Tabular.text_file_format`,
|
|
87
|
+
# unless a stream reads the lines in a format of its own, such as the CSV of a spreadsheet, which v3.0 reads them
|
|
88
|
+
# in, whatever format the file name names. Returns nil otherwise.
|
|
89
|
+
def text_file_format
|
|
90
|
+
return if format_set? || file_name.nil? || text_format
|
|
91
|
+
|
|
92
|
+
Tabular.text_file_format(file_name)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Returns [Symbol|nil] the tabular format of the data that a copy without a mode reads from these streams, or
|
|
96
|
+
# writes to them, when IOStreams v3.0 converts the rows of such a copy between it and another format, see
|
|
97
|
+
# `IOStreams::Stream#warn_copy_format`: the format of the text that the streams read, see #text_format, such as
|
|
98
|
+
# `:csv` for a spreadsheet, otherwise #format. Returns nil when v3.0 copies the data as it is: when the format is
|
|
99
|
+
# not known, such as for an IO without a file name or a `.txt` file, when the data is read and written as it is
|
|
100
|
+
# stored, see #raw, for a file that v3.0 reads in its `:text` format, see #text_file_format, and for a format whose
|
|
101
|
+
# rows are Ruby objects rather than lines of text, such as `:array` and `:hash`, see `IOStreams::Tabular.text_rows?`.
|
|
102
|
+
def copy_format
|
|
103
|
+
return if raw? || text_file_format
|
|
104
|
+
|
|
105
|
+
format = text_format || self.format
|
|
106
|
+
format if format && Tabular.text_rows?(format)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
# Reports a deprecation warning for supplying `:none`, see #raw, or for setting the encode stream, see #encoding,
|
|
112
|
+
# which names the method that the application called.
|
|
113
|
+
def warn_deprecated_stream(method, stream)
|
|
114
|
+
case stream
|
|
115
|
+
when :none
|
|
116
|
+
Deprecation.warn("#{method}(:none) is deprecated, use #raw to read or write the data as it is stored, " \
|
|
117
|
+
"or #file_name to read a file that is not named for its format")
|
|
118
|
+
when :encode
|
|
119
|
+
# Like any other stream set with #stream, stream(:encode, ...) stops the streams being taken from the file
|
|
120
|
+
# name, which #encoding keeps.
|
|
121
|
+
if method == "stream"
|
|
122
|
+
Deprecation.warn("stream(:encode, ...) is deprecated, use #encoding instead, which keeps the streams that " \
|
|
123
|
+
"the file name implies, #raw to read or write the data as it is stored, or #file_name to " \
|
|
124
|
+
"read a file that is not named for its format")
|
|
125
|
+
else
|
|
126
|
+
Deprecation.warn("#{method}(:encode, ...) is deprecated, use #encoding instead")
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Reports a deprecation warning for each option of the stream that IOStreams 3.0 raises ArgumentError for when it is
|
|
132
|
+
# set with #option, #stream or #option_or_stream, whether or not the file name includes the stream, so that it is
|
|
133
|
+
# reported wherever the code runs, rather than only where a file name includes the stream: one that the reader or
|
|
134
|
+
# the writer of the stream replaces, with the option that replaces it, see
|
|
135
|
+
# `IOStreams::Deprecation::OptionNames#replaced_option_names`, and one that neither of them takes in IOStreams 3.0,
|
|
136
|
+
# such as a misspelled one, see #v3_option_names. An option of the other direction is not reported, since v3.0
|
|
137
|
+
# accepts it, and ignores it, nor are the options of the encode stream, which is deprecated itself, see
|
|
138
|
+
# #warn_deprecated_stream, or of `:none`, which takes none.
|
|
139
|
+
def warn_deprecated_options(stream, options)
|
|
140
|
+
return if options.empty? || %i[encode none].include?(stream)
|
|
141
|
+
|
|
142
|
+
classes = v3_stream_classes(stream)
|
|
143
|
+
replaced = classes.each_with_object({}) do |klass, names|
|
|
144
|
+
names.merge!(klass.replaced_option_names) if klass.respond_to?(:replaced_option_names)
|
|
145
|
+
end.slice(*options.keys)
|
|
146
|
+
replaced.each_pair { |name, replacement| Deprecation.warn("#{name}: is deprecated, use #{replacement}: instead") }
|
|
147
|
+
|
|
148
|
+
known = v3_option_names(classes)
|
|
149
|
+
names = known ? options.keys - replaced.keys - known : []
|
|
150
|
+
return if names.empty?
|
|
151
|
+
|
|
152
|
+
words = Deprecation::Words.new(names)
|
|
153
|
+
Deprecation.warn("#{words.keywords} #{words.are} deprecated for a #{stream.inspect} stream, since neither its " \
|
|
154
|
+
"reader nor its writer takes #{words.them}, so correct or remove #{words.them}")
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Returns [Array<Class|Module>] the classes that read and write the stream in IOStreams 3.0, as far as they are
|
|
158
|
+
# known: the reader and the writer registered for it, and, in place of a writer that it does not have, the stand-in
|
|
159
|
+
# for the one that IOStreams 3.0 adds, when its reader names one, such as for `.xlsx`, see
|
|
160
|
+
# `IOStreams::Xlsx::Reader.v3_writer`. None when the stream is not registered.
|
|
161
|
+
def v3_stream_classes(stream)
|
|
162
|
+
extension = IOStreams.extensions[stream]
|
|
163
|
+
return [] unless extension
|
|
164
|
+
|
|
165
|
+
reader = extension.reader_class
|
|
166
|
+
writer = extension.writer_class
|
|
167
|
+
writer ||= reader.v3_writer if reader.respond_to?(:v3_writer)
|
|
168
|
+
[reader, writer].compact
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Returns [Array<Symbol>|nil] the options that IOStreams 3.0 accepts when they are set for a stream of the supplied
|
|
172
|
+
# classes, see #v3_stream_classes: those that its reader or its writer takes, or nil when they are not known, since
|
|
173
|
+
# a class does not declare them, as one that an application registers need not.
|
|
174
|
+
def v3_option_names(classes)
|
|
175
|
+
names = classes.map { |klass| v3_class_option_names(klass) }
|
|
176
|
+
names.flatten.uniq unless names.empty? || names.include?(nil)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Returns [Array<Symbol>|nil] the options that the class takes in IOStreams 3.0, see
|
|
180
|
+
# `IOStreams::Deprecation::OptionNames#known_option_names`, or those that it declares when it does not answer them,
|
|
181
|
+
# as a class that an application registers need not, or nil when it does neither.
|
|
182
|
+
def v3_class_option_names(klass)
|
|
183
|
+
return klass.known_option_names if klass.respond_to?(:known_option_names)
|
|
184
|
+
|
|
185
|
+
klass.option_names if klass.respond_to?(:option_names)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Reports a deprecation warning, when the streams are opened, for those that are deprecated, or whose results
|
|
189
|
+
# change in IOStreams v3.0.
|
|
190
|
+
def warn_deprecated_streams
|
|
191
|
+
warn_encode_extension
|
|
192
|
+
warn_temp_file_streams
|
|
193
|
+
warn_extension_options
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# A file name ending in `.encode` is deprecated, since file names do not name the encode stream, see #encoding.
|
|
197
|
+
# Once the encode stream is set, with #encoding or `option(:encode, ...)`, the file name adds nothing to it, so it
|
|
198
|
+
# is only reported when the file name is what applies the encode stream.
|
|
199
|
+
def warn_encode_extension
|
|
200
|
+
return if streams || @encoding || options&.key?(:encode)
|
|
201
|
+
return if file_name.nil? || !parse_extensions.include?(:encode)
|
|
202
|
+
|
|
203
|
+
Deprecation.warn("a file name ending in .encode is deprecated, since file names do not name the encode " \
|
|
204
|
+
"stream, use #encoding instead")
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# The path of `IOStreams.temp_file` reads and writes its data as it is stored, see #no_streams, while IOStreams v3.0
|
|
208
|
+
# applies the streams that its file name implies, so reading or writing it is deprecated while it has no streams of
|
|
209
|
+
# its own, and its file name implies some, other than `.encode`, which v3.0 no longer applies, see #raw, which reads
|
|
210
|
+
# and writes the data as it is stored on both.
|
|
211
|
+
def warn_temp_file_streams
|
|
212
|
+
return unless @temp_file && streams.empty? && file_name
|
|
213
|
+
return if (parse_extensions - [:encode]).empty?
|
|
214
|
+
|
|
215
|
+
Deprecation.warn("reading or writing the path of IOStreams.temp_file is deprecated when its extension names " \
|
|
216
|
+
"streams, such as .gz, since v3.0 applies them, where v2.2 reads and writes the data as it is " \
|
|
217
|
+
"stored: call #raw on the path to read and write the data as it is stored")
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# The options set for another extension of the format of an extension in the file name, such as
|
|
221
|
+
# `option(:pgp, ...)` for a `.gpg` file, are ignored, while IOStreams v3.0 applies the options of a format to each of
|
|
222
|
+
# its extensions, and then those set for the extension in the file name, which take precedence. So they are
|
|
223
|
+
# deprecated unless those of the extension itself set each of them too, with which both apply the same options.
|
|
224
|
+
def warn_extension_options
|
|
225
|
+
return if streams || options.nil? || file_name.nil?
|
|
226
|
+
|
|
227
|
+
parse_extensions.each do |stream|
|
|
228
|
+
own = options[stream] || {}
|
|
229
|
+
options.each_pair do |name, opts|
|
|
230
|
+
next if name == stream || (opts.keys - own.keys).empty? || !same_format?(name, stream)
|
|
231
|
+
|
|
232
|
+
Deprecation.warn("option(:#{name}, ...) is deprecated for a file name with the extension .#{stream}, which " \
|
|
233
|
+
"v2.2 does not apply it to, since v3.0 applies the options of a format to each of its " \
|
|
234
|
+
"extensions: set them with option(:#{stream}, ...)")
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Returns [true|false] whether the extensions are of the same format, whose reader and writer they share, such as
|
|
240
|
+
# `.pgp` and `.gpg`, or `.gz` and `.gzip`.
|
|
241
|
+
def same_format?(extension, other)
|
|
242
|
+
registered = IOStreams.extensions[extension]
|
|
243
|
+
other = IOStreams.extensions[other]
|
|
244
|
+
return false if registered.nil? || other.nil?
|
|
245
|
+
|
|
246
|
+
registered.reader_class == other.reader_class && registered.writer_class == other.writer_class
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
data/lib/io_streams/builder.rb
CHANGED
|
@@ -1,86 +1,192 @@
|
|
|
1
|
+
require "io_streams/builder/deprecations"
|
|
2
|
+
|
|
1
3
|
module IOStreams
|
|
2
4
|
# Build the streams that need to be applied to a path during reading or writing.
|
|
3
5
|
class Builder
|
|
6
|
+
include Deprecations
|
|
7
|
+
|
|
4
8
|
attr_accessor :file_name, :format_options
|
|
5
9
|
attr_reader :streams, :options
|
|
6
10
|
|
|
11
|
+
# Returns [Hash|String|IOStreams::Stream|nil] the schema of the rows and records as it was set, see #schema=.
|
|
12
|
+
attr_reader :schema
|
|
13
|
+
|
|
7
14
|
def initialize(file_name = nil)
|
|
8
15
|
@file_name = file_name
|
|
9
16
|
@streams = nil
|
|
10
17
|
@options = nil
|
|
11
|
-
@
|
|
18
|
+
@named_format = nil
|
|
19
|
+
@set_format = nil
|
|
12
20
|
@format_option = nil
|
|
21
|
+
@encoding = nil
|
|
22
|
+
@raw = false
|
|
23
|
+
@temp_file = false
|
|
24
|
+
@schema = nil
|
|
25
|
+
@schema_source = nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# A copy has its own streams, options and encoding, so that changing them does not change the original. It shares
|
|
29
|
+
# the schema, which #schema= replaces rather than changes, so that the copies read its file once, see
|
|
30
|
+
# `IOStreams::Schema::Source`.
|
|
31
|
+
def initialize_copy(source)
|
|
32
|
+
super
|
|
33
|
+
@streams = @streams&.transform_values(&:dup)
|
|
34
|
+
@options = @options&.transform_values(&:dup)
|
|
35
|
+
@encoding = @encoding&.dup
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Sets the schema of the rows and records, see `IOStreams::Stream#schema`: a Hash, which is checked now, or the path
|
|
39
|
+
# or url of a JSON file that holds one, which is read the first time that a reader or writer needs it, see
|
|
40
|
+
# `IOStreams::Schema::Source`, or nil to remove it.
|
|
41
|
+
#
|
|
42
|
+
# A copy of a Hash or String is held, which is frozen, so that the caller changing the object that it supplied
|
|
43
|
+
# changes neither #schema nor the schema that is read, and a copy of a path, so that changing its path does not
|
|
44
|
+
# change the schema.
|
|
45
|
+
#
|
|
46
|
+
# Raises ArgumentError when the schema is not a Hash, a String or a path, or is a Hash that is not a valid schema.
|
|
47
|
+
def schema=(schema)
|
|
48
|
+
schema = schema.is_a?(IOStreams::Stream) ? schema.dup : Utils.frozen_copy(schema)
|
|
49
|
+
@schema_source = schema.nil? ? nil : IOStreams::Schema::Source.new(schema)
|
|
50
|
+
@schema = schema
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Returns [IOStreams::Schema|nil] the schema of the rows and records, reading its file the first time, see #schema=,
|
|
54
|
+
# or nil when none is set.
|
|
55
|
+
def tabular_schema
|
|
56
|
+
@schema_source&.schema
|
|
13
57
|
end
|
|
14
58
|
|
|
15
59
|
# Supply an option that is only applied once the file name extensions have been parsed.
|
|
16
60
|
# Note:
|
|
17
61
|
# - Cannot set both `stream` and `option`
|
|
62
|
+
#
|
|
63
|
+
# Setting the encode stream with `option(:encode, ...)` is deprecated, see #encoding, and reports a deprecation
|
|
64
|
+
# warning, see `IOStreams::Deprecation`.
|
|
18
65
|
def option(stream, **options)
|
|
19
66
|
stream = stream.to_sym unless stream.is_a?(Symbol)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
@options ||= {}
|
|
25
|
-
if (opts = @options[stream])
|
|
26
|
-
opts.merge!(options)
|
|
27
|
-
else
|
|
28
|
-
@options[stream] = options.dup
|
|
29
|
-
end
|
|
67
|
+
raise_if_raw!("#option")
|
|
68
|
+
add_option(stream, options)
|
|
69
|
+
warn_deprecated_stream("option", stream)
|
|
70
|
+
warn_deprecated_options(stream, options)
|
|
30
71
|
self
|
|
31
72
|
end
|
|
32
73
|
|
|
74
|
+
# Setting the encode stream with `stream(:encode, ...)` is deprecated, see #encoding. Like any other stream set
|
|
75
|
+
# with `#stream`, it still stops the streams being taken from the file name, which #encoding keeps.
|
|
76
|
+
#
|
|
77
|
+
# Supplying `:none` is deprecated, see #raw. Since it does what #raw does, it can still be supplied after #raw.
|
|
78
|
+
#
|
|
79
|
+
# Each reports a deprecation warning, see `IOStreams::Deprecation`.
|
|
33
80
|
def stream(stream, **options)
|
|
34
81
|
stream = stream.to_sym unless stream.is_a?(Symbol)
|
|
35
|
-
|
|
82
|
+
raise_if_raw!("#stream") unless stream == :none
|
|
83
|
+
add_stream(stream, options)
|
|
84
|
+
warn_deprecated_stream("stream", stream)
|
|
85
|
+
warn_deprecated_options(stream, options)
|
|
86
|
+
self
|
|
87
|
+
end
|
|
36
88
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
89
|
+
# Sets the encoding of the text that the application reads or writes, and the other options of the encode
|
|
90
|
+
# stream, see `IOStreams::Encode`, merging them with those already set.
|
|
91
|
+
#
|
|
92
|
+
# Unlike `#option`, it can be combined with `#stream`, and needs no file name. Unlike `#stream`, it does not stop
|
|
93
|
+
# the streams being taken from the file name. The encode stream is applied closest to the application, after
|
|
94
|
+
# every other stream when reading, with the options set by `#option` or `#stream` for `:encode`, if any, and
|
|
95
|
+
# these on top of them.
|
|
96
|
+
#
|
|
97
|
+
# Raises ArgumentError when neither an encoding nor any options are supplied, when the encoding is supplied both
|
|
98
|
+
# as an argument and as the `encoding:` option, or for an option that the encode stream does not take, or a value
|
|
99
|
+
# that it does not accept, see `IOStreams::Encode.check`. They are checked when they are set, since a writer that
|
|
100
|
+
# raised for them would have replaced its file. The options set with `#option` or `#stream` for `:encode` are
|
|
101
|
+
# deprecated, and are still only checked when the stream is opened, as before.
|
|
102
|
+
def encoding(encoding = nil, **options)
|
|
103
|
+
raise_if_raw!("#encoding")
|
|
104
|
+
if encoding
|
|
105
|
+
raise(ArgumentError, "Supply the encoding as an argument or as `encoding:`, not both") if options.key?(:encoding)
|
|
106
|
+
|
|
107
|
+
options = {encoding: encoding, **options}
|
|
41
108
|
end
|
|
42
|
-
raise(ArgumentError, "
|
|
109
|
+
raise(ArgumentError, "Supply the encoding, or options for the encode stream") if options.empty?
|
|
43
110
|
|
|
44
|
-
@
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
else
|
|
48
|
-
@streams[stream] = options.dup
|
|
49
|
-
end
|
|
111
|
+
merged = (@encoding || {}).merge(options)
|
|
112
|
+
Encode.check(**merged)
|
|
113
|
+
@encoding = merged
|
|
50
114
|
self
|
|
51
115
|
end
|
|
52
116
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
117
|
+
# Reads and writes the data as it is stored: applies no streams, neither those that the file name implies nor any
|
|
118
|
+
# already set with #stream or #option, and no encoding set with #encoding. For example, to download a zip file
|
|
119
|
+
# without unzipping it, or to write data that is already compressed.
|
|
120
|
+
#
|
|
121
|
+
# Since the data is as stored, setting a stream, option or encoding afterwards raises ArgumentError.
|
|
122
|
+
def raw
|
|
123
|
+
no_streams
|
|
124
|
+
@options = nil
|
|
125
|
+
@raw = true
|
|
126
|
+
self
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Returns [true|false] whether the data is read and written as it is stored, see #raw.
|
|
130
|
+
def raw?
|
|
131
|
+
@raw
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Returns [true|false] whether the text that the streams yield is taken as UTF-8: the text of a file with a
|
|
135
|
+
# schema, see #schema=, unless the application chose the encoding of its text, with an encode stream, whether set
|
|
136
|
+
# with #encoding, #option, #stream, or by a file name ending in `.encode`, or chose none with #raw, which reads the
|
|
137
|
+
# data as it is stored. Without a schema, the text is read as it always was.
|
|
138
|
+
def utf8_text?
|
|
139
|
+
!@schema.nil? && !raw? && !pipeline.key?(:encode)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Applies no streams, including an encoding set with #encoding, as `stream(:none)` does, for IOStreams itself.
|
|
143
|
+
#
|
|
144
|
+
# `temp_file: true` for the path of `IOStreams.temp_file`, which reads and writes its data as it is stored, while
|
|
145
|
+
# IOStreams v3.0 applies the streams that its file name implies, so that reading or writing it reports a deprecation
|
|
146
|
+
# warning while they would apply, see #warn_temp_file_streams.
|
|
147
|
+
def no_streams(temp_file: false)
|
|
148
|
+
@streams = {}
|
|
149
|
+
@encoding = nil
|
|
150
|
+
@temp_file = temp_file
|
|
151
|
+
self
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Sets the options of a stream as #option does, unless streams were already set with #stream, or no file name is
|
|
155
|
+
# set, when it adds the stream as #stream does.
|
|
156
|
+
#
|
|
157
|
+
# Setting the encode stream with `option_or_stream(:encode, ...)` is deprecated, see #encoding, and reports a
|
|
158
|
+
# deprecation warning, see `IOStreams::Deprecation`.
|
|
159
|
+
def option_or_stream(stream, **options)
|
|
160
|
+
stream = stream.to_sym unless stream.is_a?(Symbol)
|
|
161
|
+
raise_if_raw!("#option_or_stream") unless stream == :none
|
|
162
|
+
streams || file_name.nil? ? add_stream(stream, options) : add_option(stream, options)
|
|
163
|
+
warn_deprecated_stream("option_or_stream", stream)
|
|
164
|
+
warn_deprecated_options(stream, options)
|
|
165
|
+
self
|
|
61
166
|
end
|
|
62
167
|
|
|
63
168
|
# Return the options set for either a stream or option.
|
|
64
169
|
def setting(stream)
|
|
170
|
+
return encode_setting if stream == :encode && @encoding
|
|
65
171
|
return streams[stream] if streams
|
|
66
172
|
|
|
67
173
|
options[stream] if options
|
|
68
174
|
end
|
|
69
175
|
|
|
70
176
|
def reader(io_stream, &)
|
|
177
|
+
warn_deprecated_streams
|
|
71
178
|
execute(:reader, pipeline, io_stream, &)
|
|
72
179
|
end
|
|
73
180
|
|
|
74
181
|
def writer(io_stream, &)
|
|
182
|
+
warn_deprecated_streams
|
|
75
183
|
execute(:writer, pipeline, io_stream, &)
|
|
76
184
|
end
|
|
77
185
|
|
|
78
186
|
# Returns [Hash<Symbol:Hash>] the pipeline of streams
|
|
79
187
|
# with their options that will be applied when the reader or writer is invoked.
|
|
80
188
|
def pipeline
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
build_pipeline.freeze
|
|
189
|
+
with_encoding(streams ? streams.dup : build_pipeline).freeze
|
|
84
190
|
end
|
|
85
191
|
|
|
86
192
|
# Removes the named stream from the current pipeline.
|
|
@@ -88,21 +194,43 @@ module IOStreams
|
|
|
88
194
|
# Note: Any options must be set _before_ calling this method.
|
|
89
195
|
def remove_from_pipeline(stream_name)
|
|
90
196
|
@streams ||= build_pipeline
|
|
197
|
+
@encoding = nil if stream_name.to_sym == :encode
|
|
198
|
+
# The streams are now those that the application chose, see #warn_temp_file_streams.
|
|
199
|
+
@temp_file = false
|
|
91
200
|
@streams.delete(stream_name.to_sym)
|
|
92
201
|
end
|
|
93
202
|
|
|
94
203
|
# Returns the tabular format if set, otherwise tries to autodetect the format if the file_name has been set
|
|
95
204
|
# Returns [nil] if no format is set, or if it cannot be determined from the file_name
|
|
205
|
+
#
|
|
206
|
+
# The format that the file name names is kept once it is found, until a format is set, see #format=.
|
|
96
207
|
def format
|
|
97
|
-
@
|
|
208
|
+
@set_format || (@named_format ||= file_name ? Tabular.format_from_file_name(file_name) : nil)
|
|
98
209
|
end
|
|
99
210
|
|
|
211
|
+
# Sets the tabular format, or nil to detect it from the file name again, see #format.
|
|
100
212
|
def format=(format)
|
|
101
213
|
unless format.nil? || IOStreams::Tabular.registered_formats.include?(format)
|
|
102
214
|
raise(ArgumentError, "Invalid format: #{format.inspect}")
|
|
103
215
|
end
|
|
104
216
|
|
|
105
|
-
@
|
|
217
|
+
@set_format = format
|
|
218
|
+
@named_format = nil
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Returns [true|false] whether a format was set, see #format=, rather than taken from the file name, see #format.
|
|
222
|
+
def format_set?
|
|
223
|
+
!@set_format.nil?
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Returns [Symbol|nil] the tabular format of the text that the stream closest to the application reads, when that
|
|
227
|
+
# stream decides it, such as `:csv` for a spreadsheet, whose rows it reads as CSV, see
|
|
228
|
+
# `IOStreams::Xlsx::Reader.text_format`, otherwise nil, for a stream whose data is the text, such as gzip. The encode
|
|
229
|
+
# stream converts that text, so it is the stream after it.
|
|
230
|
+
def text_format
|
|
231
|
+
stream = pipeline.each_key.find { |name| name != :encode }
|
|
232
|
+
klass = stream && IOStreams.extensions[stream]&.reader_class
|
|
233
|
+
klass.text_format if klass.respond_to?(:text_format)
|
|
106
234
|
end
|
|
107
235
|
|
|
108
236
|
# Returns [String] the quote character within which field delimiters and newlines may be
|
|
@@ -117,8 +245,68 @@ module IOStreams
|
|
|
117
245
|
format && IOStreams::Tabular.parser_class(format).quote_character
|
|
118
246
|
end
|
|
119
247
|
|
|
248
|
+
# Returns [Hash<Symbol, Array<Symbol>>] the settings of this stream that apply to what it writes, other than the
|
|
249
|
+
# options of each call: `streams:` the names of the streams of its pipeline, see #pipeline, which #stream, #option,
|
|
250
|
+
# #encoding and the file name set, and `tabular:` the names of those of its rows and records that were set: `:format`,
|
|
251
|
+
# when it was set, see #format=, rather than taken from the file name, and `:schema`, see #schema=.
|
|
252
|
+
def writer_settings
|
|
253
|
+
tabular = []
|
|
254
|
+
tabular << :format if format_set?
|
|
255
|
+
tabular << :schema unless schema.nil?
|
|
256
|
+
{streams: pipeline.keys, tabular: tabular}
|
|
257
|
+
end
|
|
258
|
+
|
|
120
259
|
private
|
|
121
260
|
|
|
261
|
+
# Sets the options of a stream that is applied once the file name extensions have been parsed, see #option.
|
|
262
|
+
def add_option(stream, options)
|
|
263
|
+
raise(ArgumentError, "Invalid stream: #{stream.inspect}") unless IOStreams.extensions.include?(stream)
|
|
264
|
+
raise(ArgumentError, "Cannot call both #option and #stream on the same streams instance}") if @streams
|
|
265
|
+
raise(ArgumentError, "Cannot call #option unless the `file_name` was already set}") unless file_name
|
|
266
|
+
|
|
267
|
+
@options ||= {}
|
|
268
|
+
if (opts = @options[stream])
|
|
269
|
+
opts.merge!(options)
|
|
270
|
+
else
|
|
271
|
+
@options[stream] = options.dup
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Adds a stream, in place of the streams that the file name implies, see #stream.
|
|
276
|
+
def add_stream(stream, options)
|
|
277
|
+
raise(ArgumentError, "Cannot call both #option and #stream on the same streams instance}") if @options
|
|
278
|
+
# `:none` applies no streams, including an encoding set with #encoding. It is the deprecated form of #raw.
|
|
279
|
+
return no_streams if stream == :none
|
|
280
|
+
raise(ArgumentError, "Invalid stream: #{stream.inspect}") unless IOStreams.extensions.include?(stream)
|
|
281
|
+
|
|
282
|
+
@streams ||= {}
|
|
283
|
+
if (opts = @streams[stream])
|
|
284
|
+
opts.merge!(options)
|
|
285
|
+
else
|
|
286
|
+
@streams[stream] = options.dup
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def raise_if_raw!(method_name)
|
|
291
|
+
return unless @raw
|
|
292
|
+
|
|
293
|
+
raise(ArgumentError, "Cannot call #{method_name} after #raw, which reads and writes the data as it is stored")
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Returns [Hash] the options of the encode stream set with #option or #stream, if any, and those of #encoding.
|
|
297
|
+
def encode_setting
|
|
298
|
+
setting = streams ? streams[:encode] : options&.[](:encode)
|
|
299
|
+
(setting || {}).merge(@encoding)
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Returns [Hash<Symbol:Hash>] the pipeline, with the encode stream first, closest to the application, when an
|
|
303
|
+
# encoding was set with #encoding.
|
|
304
|
+
def with_encoding(pipeline)
|
|
305
|
+
return pipeline unless @encoding
|
|
306
|
+
|
|
307
|
+
{encode: encode_setting}.merge(pipeline.except(:encode))
|
|
308
|
+
end
|
|
309
|
+
|
|
122
310
|
def build_pipeline
|
|
123
311
|
return {} unless file_name
|
|
124
312
|
|
|
@@ -139,7 +327,7 @@ module IOStreams
|
|
|
139
327
|
|
|
140
328
|
# Returns the streams for the supplied file_name
|
|
141
329
|
def parse_extensions
|
|
142
|
-
parts = ::
|
|
330
|
+
parts = Utils::PathName.basename(file_name).split(".")
|
|
143
331
|
extensions = []
|
|
144
332
|
while (extension = parts.pop)
|
|
145
333
|
sym = extension.downcase.to_sym
|
|
@@ -169,9 +357,38 @@ module IOStreams
|
|
|
169
357
|
end
|
|
170
358
|
|
|
171
359
|
def open_stream(type, stream, io_stream, opts, &)
|
|
172
|
-
klass =
|
|
360
|
+
klass = stream_class(type, stream)
|
|
173
361
|
validate_options(type, stream, klass, opts)
|
|
174
|
-
klass.open(io_stream, **opts, &)
|
|
362
|
+
klass.open(io_stream, **taken_options(type, stream, klass, opts), &)
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# Returns [Class] the class that reads or writes the stream: the class registered for it, or, to read a file with a
|
|
366
|
+
# schema, see #schema=, the class that the registered reader answers for it, when it reads a file with a schema
|
|
367
|
+
# differently, such as the reader of a spreadsheet, see `IOStreams::Xlsx::Reader.schema_reader`.
|
|
368
|
+
def stream_class(type, stream)
|
|
369
|
+
klass = class_for_stream(type, stream)
|
|
370
|
+
return klass if type != :reader || @schema.nil? || !klass.respond_to?(:schema_reader)
|
|
371
|
+
|
|
372
|
+
klass.schema_reader
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# Returns [Hash] the options of the stream that the class is opened with: all of them, unless the class takes any
|
|
376
|
+
# option, since it does not declare them, see #validate_options, while it answers those that it takes in IOStreams
|
|
377
|
+
# 3.0, as `IOStreams::Bzip2::Reader` does until v3.0, see #v3_class_option_names. It is then opened with those
|
|
378
|
+
# alone, and a warning is logged for each of the others, which it would ignore, see
|
|
379
|
+
# `IOStreams::Utils.warn_unknown_options`, naming the stream as it was set: one that the class of the other
|
|
380
|
+
# direction takes, which v3.0 accepts, and ignores, or one that neither takes, which v3.0 raises ArgumentError for.
|
|
381
|
+
def taken_options(type, stream, klass, opts)
|
|
382
|
+
known = v3_class_option_names(klass) if option_names(klass).nil?
|
|
383
|
+
return opts if known.nil?
|
|
384
|
+
|
|
385
|
+
unknown = opts.except(*known)
|
|
386
|
+
return opts if unknown.empty?
|
|
387
|
+
|
|
388
|
+
other = IOStreams.extensions[stream].public_send(type == :reader ? :writer_class : :reader_class)
|
|
389
|
+
other_names = (v3_class_option_names(other) if other) || []
|
|
390
|
+
Utils.warn_unknown_options(unknown, stream, type, known, other_names)
|
|
391
|
+
opts.slice(*known)
|
|
175
392
|
end
|
|
176
393
|
|
|
177
394
|
# Options are strict: an option the stream does not accept raises instead of being ignored.
|