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,361 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IOStreams
|
|
4
|
+
class Tabular
|
|
5
|
+
# Maps the columns of a file to the fields of its schema, see `IOStreams::Tabular::Mapping`: the columns of its header
|
|
6
|
+
# row, or the keys of each record of a format of keyed records, such as JSON, are matched to the fields once, see
|
|
7
|
+
# `IOStreams::Schema::Columns`, which converts the values of each row and record read to the types of the fields, by
|
|
8
|
+
# their names, and renders those written. The columns of the rows of a file without a header row are the fields, in
|
|
9
|
+
# order, see `IOStreams::Schema#header?`, including those of a format whose records supply their own keys, whose rows
|
|
10
|
+
# are the values of every field of each record.
|
|
11
|
+
#
|
|
12
|
+
# The column options of a file without a schema do not apply, since the schema describes the columns, see
|
|
13
|
+
# `IOStreams::Tabular.new`. The header keeps the header row as it was read, so that another Tabular with the same
|
|
14
|
+
# schema, such as one that reads the rest of the file elsewhere, matches the same columns with
|
|
15
|
+
# `read_header(header.columns)`.
|
|
16
|
+
#
|
|
17
|
+
# Its text is UTF-8, as IOStreams 3.0 reads and writes the text of every file by default: a line read as binary,
|
|
18
|
+
# as a file is read without an encoding, or a row already read, is taken as UTF-8, unless the text is already in the
|
|
19
|
+
# encoding that the application chose, see `decoded:`, and so is binary text written, such as the values of rows read
|
|
20
|
+
# from a file without a schema, see `IOStreams::Utils.utf8`, unless it is not valid UTF-8 and the application chose
|
|
21
|
+
# an encoding, which encodes it instead, see #written_row.
|
|
22
|
+
class SchemaMapping < Mapping
|
|
23
|
+
# Parameters:
|
|
24
|
+
# schema: [IOStreams::Schema]
|
|
25
|
+
# The fields of the rows and records.
|
|
26
|
+
#
|
|
27
|
+
# decoded: [true|false]
|
|
28
|
+
# Whether the text is in the encoding that the application chose, rather than taken as UTF-8: with an encoding
|
|
29
|
+
# set on the stream, or with #raw, see `IOStreams::Builder#utf8_text?`. Then the lines read are taken as they
|
|
30
|
+
# are, rather than a binary line as UTF-8, see `IOStreams::Utils.utf8`, and binary text written that is not
|
|
31
|
+
# valid UTF-8 is left as it is, for that encoding, see #written_row. The lines that `IOStreams::Stream` reads
|
|
32
|
+
# without such an encoding are already UTF-8, see `IOStreams::Line::Utf8Reader`, as a caller of
|
|
33
|
+
# `IOStreams::Tabular` hands them, so they are only checked.
|
|
34
|
+
# Default: false
|
|
35
|
+
#
|
|
36
|
+
# See `IOStreams::Tabular::Mapping#initialize` for the others.
|
|
37
|
+
#
|
|
38
|
+
# Raises ArgumentError when the schema describes a file without a header row, of a format with one, such as CSV,
|
|
39
|
+
# that it cannot describe, see `IOStreams::Schema#check_fields_in_order`.
|
|
40
|
+
def initialize(schema, header:, parser:, format:, decoded: false)
|
|
41
|
+
super(header: header, parser: parser, format: format)
|
|
42
|
+
@schema = schema
|
|
43
|
+
@decoded = decoded
|
|
44
|
+
@columns = schema.columns
|
|
45
|
+
@requires_header = parser.requires_header? && schema.header?
|
|
46
|
+
# Whether each row is a record whose keys are its columns, such as JSON, rather than the values in the order of
|
|
47
|
+
# the columns, see `IOStreams::Tabular::Parser::Base.keyed?`, and the form in which the format writes the values
|
|
48
|
+
# of the fields.
|
|
49
|
+
@keyed = parser.class.keyed?
|
|
50
|
+
@form = parser.class.value_form
|
|
51
|
+
# The parsers render a row by a header, see `IOStreams::Tabular::Header#to_array`, which leaves a row whose
|
|
52
|
+
# values the schema has already arranged as it is, since it has no columns.
|
|
53
|
+
@arranged = Header.new.freeze
|
|
54
|
+
# The columns of the rows of a file without a header row are the fields, in order: of a format with one whose
|
|
55
|
+
# schema says that it has none, see `IOStreams::Schema#check_fields_in_order`, or of a format without one, such
|
|
56
|
+
# as fixed width, or JSON, whose rows are the values of the fields of its records.
|
|
57
|
+
if parser.requires_header?
|
|
58
|
+
@columns.match_fields unless schema.header?
|
|
59
|
+
else
|
|
60
|
+
@columns.match_in_order
|
|
61
|
+
end
|
|
62
|
+
# Whether the names of the fields have been read as the header row of the rows of a file without one, see
|
|
63
|
+
# #rows_header?.
|
|
64
|
+
@names_read = false
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Returns [true|false] whether the file has a header row: when its format has one, such as CSV, unless the schema
|
|
68
|
+
# says that the file has none, see `IOStreams::Schema#header?`.
|
|
69
|
+
def requires_header?
|
|
70
|
+
@requires_header
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Returns [true|false] whether the header row is still to be read, see #read_header: until its columns are
|
|
74
|
+
# matched to the fields, for a file with a header row.
|
|
75
|
+
def header?
|
|
76
|
+
@requires_header && !@columns.matched?
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Returns [true|false] whether the rows read start with a header row, see `IOStreams::Tabular#rows_header?`: the
|
|
80
|
+
# header row of a file with one, until it is read, see #header?, or for a file without one, the names of the
|
|
81
|
+
# fields, which are the columns of its rows, until they are read, see #read_row, so that the rows of every file
|
|
82
|
+
# start with a header row.
|
|
83
|
+
def rows_header?
|
|
84
|
+
@requires_header ? header? : !@names_read
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Returns [false] the rows read are Arrays, see `IOStreams::Tabular#record_rows?`, after a header row of the names
|
|
88
|
+
# of the fields of a file without one, such as JSON, see #rows_header?.
|
|
89
|
+
def record_rows?
|
|
90
|
+
false
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Returns [Array<String|Symbol>] the names of the fields of a file without a header row, which are the columns of
|
|
94
|
+
# its rows, see `IOStreams::Tabular#headerless_columns`.
|
|
95
|
+
# Returns nil for a file with a header row.
|
|
96
|
+
def headerless_columns
|
|
97
|
+
@columns.names if @columns.matched? && !@requires_header
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Returns [nil] the key of each value of a record is its own field, since two columns of the same field raise
|
|
101
|
+
# `IOStreams::Errors::InvalidHeader`, see `IOStreams::Tabular#repeated_key`.
|
|
102
|
+
def repeated_key
|
|
103
|
+
nil
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Returns [Array] the header row parsed from the line, once its columns are matched to the fields, see
|
|
107
|
+
# `IOStreams::Schema::Columns#match`.
|
|
108
|
+
# Returns nil when the line is blank, or the file has no header row, see #requires_header?, such as with a schema
|
|
109
|
+
# whose `header` is false, whose lines are all rows.
|
|
110
|
+
def parse_header(line)
|
|
111
|
+
match_header(line)&.first
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Matches the columns of the header row to the fields, see #parse_header.
|
|
115
|
+
#
|
|
116
|
+
# Returns [Array<String|Symbol>] the names of the fields of its columns, in the order of the fields.
|
|
117
|
+
# Returns nil when the line is blank, or the file has no header row.
|
|
118
|
+
#
|
|
119
|
+
# Raises ArgumentError for `cleanse` that is supplied, other than nil, which leaves the header row as it is, since
|
|
120
|
+
# the keys of the records are the names of the fields, which its columns are matched to instead.
|
|
121
|
+
def read_header(line, cleanse: CLEANSE_BY_DEFAULT)
|
|
122
|
+
unless cleanse.nil? || cleanse.equal?(CLEANSE_BY_DEFAULT)
|
|
123
|
+
raise(ArgumentError, "cleanse: cannot be supplied with a schema, since the keys of its records are the names " \
|
|
124
|
+
"of its fields, which the columns of the header row are matched to, see " \
|
|
125
|
+
"IOStreams::Schema#field_for")
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
match_header(line)&.last
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Returns [Array] the values of the fields of the line, converted to their types, in the order of the fields: of
|
|
132
|
+
# its columns, or, for a format whose records supply their own keys, such as JSON, of every field, nil for a key
|
|
133
|
+
# that its record does not have, see `IOStreams::Schema::Columns#read_row`.
|
|
134
|
+
# Returns nil if the line is blank.
|
|
135
|
+
def row_parse(line)
|
|
136
|
+
values = parse(line)
|
|
137
|
+
return if values.nil?
|
|
138
|
+
|
|
139
|
+
values.is_a?(::Hash) ? @columns.read_row(values) : @columns.row(values)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Returns [Hash] the record of the line, converted to the types of the fields, by the names of the fields. The
|
|
143
|
+
# record of a row already read, an Array, is by the columns of the rows, see #row_parse.
|
|
144
|
+
# Returns nil if the line is blank.
|
|
145
|
+
def record_parse(line)
|
|
146
|
+
values = parse(line)
|
|
147
|
+
return if values.nil?
|
|
148
|
+
|
|
149
|
+
values.is_a?(::Hash) ? @columns.read_hash(values) : @columns.record(values)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Returns [String|Array|Hash] the line that the parser renders for the row or record, with the value of each field
|
|
153
|
+
# in the form of its field that the format writes, see `IOStreams::Schema::Field#render`, in the order of the
|
|
154
|
+
# fields, see `IOStreams::Schema::Columns#render`.
|
|
155
|
+
#
|
|
156
|
+
# Returns nil for a blank row, which is written as a blank line, unless the schema has a field that is required,
|
|
157
|
+
# which raises `IOStreams::Errors::InvalidValue`, since the row has no value for it.
|
|
158
|
+
def render(row)
|
|
159
|
+
return @columns.render_blank if IOStreams::Utils.blank?(row)
|
|
160
|
+
|
|
161
|
+
@parser.render(@columns.render(written_row(row), @form, @keyed), @arranged)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Returns [String] the header row of the file: the names of its fields.
|
|
165
|
+
# Returns nil when the file has no header row.
|
|
166
|
+
def render_header
|
|
167
|
+
return unless @requires_header
|
|
168
|
+
|
|
169
|
+
@parser.render(@schema.fields.map { |field| field.name.to_s }, @arranged)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Yields [Array|nil] each row of the line of a file of rows: the header row, while it is expected, see #header?,
|
|
173
|
+
# as the names of the fields of its columns, see #read_header, otherwise the row, see #row_parse, or nil for a
|
|
174
|
+
# blank line. The rows of a file without a header row, such as fixed width, JSON, or a schema whose `header` is
|
|
175
|
+
# false, start with the names of the fields, before the row of the first line, since they are the columns of its
|
|
176
|
+
# rows.
|
|
177
|
+
#
|
|
178
|
+
# The header row is an `IOStreams::Row::HeaderRow`, so that a writer of rows takes it as the header row of the
|
|
179
|
+
# rows after it, see `IOStreams::Row::Writer#<<`.
|
|
180
|
+
#
|
|
181
|
+
# Returns the result of the block.
|
|
182
|
+
def read_row(line)
|
|
183
|
+
# Once the header row is read, every line is a row, so that is asked first.
|
|
184
|
+
return yield(row_parse(line)) unless rows_header?
|
|
185
|
+
return yield(as_header_row(read_header(line))) if header?
|
|
186
|
+
|
|
187
|
+
@names_read = true
|
|
188
|
+
yield as_header_row(@columns.names)
|
|
189
|
+
yield row_parse(line)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Yields [Hash|nil] the record of the line of a file of records, see #record_parse, or nil for a blank line, unless
|
|
193
|
+
# the line is the header row, while it is expected, see #header?, which it reads instead, see #read_header, since
|
|
194
|
+
# it names the keys of the records after it.
|
|
195
|
+
def read_record_line(line)
|
|
196
|
+
if header?
|
|
197
|
+
read_header(line)
|
|
198
|
+
nil
|
|
199
|
+
else
|
|
200
|
+
yield record_parse(line)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Returns nil: no column options apply, since the schema describes the columns instead.
|
|
205
|
+
def restrict_columns
|
|
206
|
+
nil
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Yields [String] the header line that a writer of rows or records writes before the first one, the names of the
|
|
210
|
+
# fields, when the file has a header row, see #requires_header?, so that every row written is the values of the
|
|
211
|
+
# fields, in order, as for a file without a header row.
|
|
212
|
+
def start_rows
|
|
213
|
+
yield render_header if @requires_header
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
alias start_records start_rows
|
|
217
|
+
|
|
218
|
+
# Yields [String|nil] the line of the row, see #render.
|
|
219
|
+
#
|
|
220
|
+
# Returns the result of the block.
|
|
221
|
+
def render_row(row)
|
|
222
|
+
yield render(row)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
alias render_record render_row
|
|
226
|
+
|
|
227
|
+
# Takes the columns of the rows written after them from a header row written before them, such as the
|
|
228
|
+
# `IOStreams::Row::HeaderRow` that `IOStreams::Stream#each(:array)` yields, or the header row of a file without a
|
|
229
|
+
# schema that is copied, see `IOStreams::Tabular#copy_row`, once they are matched to the fields, see
|
|
230
|
+
# `IOStreams::Schema::Columns#match_written`, without a line, since the header row of the file is the names of the
|
|
231
|
+
# fields, which the schema writes, see #start_rows, or the file has none. Its binary text is taken as UTF-8, see
|
|
232
|
+
# #utf8_row, whatever the encoding of this stream, since the header row is read from another file, and the names
|
|
233
|
+
# of the fields are matched in Unicode.
|
|
234
|
+
#
|
|
235
|
+
# Returns nil.
|
|
236
|
+
def render_header_row(columns)
|
|
237
|
+
@columns.match_written(utf8_row(columns))
|
|
238
|
+
nil
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Raises ArgumentError: the header row is not cleansed with a schema, since the keys of the records are the names
|
|
242
|
+
# of the fields, which the columns of the header row are matched to instead, as for `cleanse_header:`, see
|
|
243
|
+
# `IOStreams::Tabular.new`. The header keeps the header row as it was read, see #read_header.
|
|
244
|
+
def cleanse_header!
|
|
245
|
+
raise(ArgumentError, "The header row cannot be cleansed with a schema, since the keys of its records are the " \
|
|
246
|
+
"names of its fields, which the columns of the header row are matched to, see " \
|
|
247
|
+
"IOStreams::Schema#field_for")
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Yields [IOStreams::Row::HeaderRow] the names of the fields of a file without a header row, which are the columns
|
|
251
|
+
# of its rows, see #headerless_columns, which a copy writes before the first line, see
|
|
252
|
+
# `IOStreams::Tabular#start_copy`, so that the target has them even when the file has no lines. #read_row then
|
|
253
|
+
# does not yield them again.
|
|
254
|
+
def start_copy
|
|
255
|
+
return if @requires_header || @names_read
|
|
256
|
+
|
|
257
|
+
@names_read = true
|
|
258
|
+
yield as_header_row(@columns.names)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Yields [Array|nil] each row of the line that a copy writes, see `IOStreams::Tabular#copy_row`, as #read_row
|
|
262
|
+
# yields it, whose header row is an `IOStreams::Row::HeaderRow`.
|
|
263
|
+
alias copy_row read_row
|
|
264
|
+
|
|
265
|
+
private
|
|
266
|
+
|
|
267
|
+
# Returns [Array(Array, Array)] the header row parsed from the line, which the header keeps as it was read, and the
|
|
268
|
+
# names of the fields of its columns, once they are matched to the fields, or nil when the line is blank, or the
|
|
269
|
+
# format has no header row.
|
|
270
|
+
def match_header(line)
|
|
271
|
+
return unless @requires_header
|
|
272
|
+
|
|
273
|
+
columns = parse(line)
|
|
274
|
+
return if columns.nil?
|
|
275
|
+
|
|
276
|
+
names = @columns.match(columns)
|
|
277
|
+
header.columns = columns
|
|
278
|
+
[columns, names]
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Returns [Array|Hash] the values of the line, which the parser parses, or a row already read, an Array, as it is,
|
|
282
|
+
# since the parsers of keyed records, such as JSON, do not accept one without a schema. Its binary text is taken
|
|
283
|
+
# as UTF-8, see `IOStreams::Utils.utf8`, whether it is a line or a row already read, unless the text is in the
|
|
284
|
+
# encoding that the application chose, see #initialize.
|
|
285
|
+
#
|
|
286
|
+
# Returns nil when it is blank, the same way in every format: a line that is blank, see
|
|
287
|
+
# `IOStreams::Utils.blank_text?`, or a row or record whose every value is blank, see `IOStreams::Utils.blank_row?`,
|
|
288
|
+
# such as a line of CSV of only commas, or a JSON record of `null`, `{}`, or whose every value is null.
|
|
289
|
+
def parse(line)
|
|
290
|
+
return if line.nil?
|
|
291
|
+
|
|
292
|
+
if line.is_a?(String)
|
|
293
|
+
line = IOStreams::Utils.utf8(line) unless @decoded
|
|
294
|
+
return if IOStreams::Utils.blank_text?(line)
|
|
295
|
+
elsif !@decoded
|
|
296
|
+
line = utf8_row(line)
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
values = line.is_a?(::Array) ? line : @parser.parse(line)
|
|
300
|
+
values unless IOStreams::Utils.blank_row?(values)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Returns [Array|Hash] the row or record, whose text that is binary and not ASCII is taken as UTF-8, see
|
|
304
|
+
# `IOStreams::Utils.utf8`, such as that of a row read from a file without a schema, which is binary, or the row
|
|
305
|
+
# itself when it has none.
|
|
306
|
+
def utf8_row(row)
|
|
307
|
+
case row
|
|
308
|
+
when ::Array
|
|
309
|
+
row.any? { |value| binary_text?(value) } ? row.map { |value| utf8_text(value) } : row
|
|
310
|
+
when ::Hash
|
|
311
|
+
return row unless row.any? { |key, value| binary_text?(key) || binary_text?(value) }
|
|
312
|
+
|
|
313
|
+
row.to_h { |key, value| [utf8_text(key), utf8_text(value)] }
|
|
314
|
+
else
|
|
315
|
+
row
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def utf8_text(value)
|
|
320
|
+
binary_text?(value) ? IOStreams::Utils.utf8(value) : value
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Returns [Array|Hash] the row or record written, see #render, whose binary text is taken as UTF-8, see #utf8_row.
|
|
324
|
+
# With an encoding set on the stream, see #initialize, which encodes the text written, binary text that is not
|
|
325
|
+
# valid UTF-8 is left as it is, for that encoding, rather than raise, as text read with it is taken as it is.
|
|
326
|
+
def written_row(row)
|
|
327
|
+
return utf8_row(row) unless @decoded
|
|
328
|
+
|
|
329
|
+
case row
|
|
330
|
+
when ::Array
|
|
331
|
+
row.any? { |value| binary_text?(value) } ? row.map { |value| written_text(value) } : row
|
|
332
|
+
when ::Hash
|
|
333
|
+
return row unless row.any? { |key, value| binary_text?(key) || binary_text?(value) }
|
|
334
|
+
|
|
335
|
+
row.to_h { |key, value| [written_text(key), written_text(value)] }
|
|
336
|
+
else
|
|
337
|
+
row
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# Returns [Object] the value written with an encoding set on the stream, see #written_row: binary text as UTF-8
|
|
342
|
+
# when it is valid UTF-8, otherwise as it is.
|
|
343
|
+
def written_text(value)
|
|
344
|
+
return value unless binary_text?(value)
|
|
345
|
+
|
|
346
|
+
text = value.dup.force_encoding(::Encoding::UTF_8)
|
|
347
|
+
text.valid_encoding? ? text : value
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def binary_text?(value)
|
|
351
|
+
value.is_a?(String) && value.encoding == ::Encoding::BINARY && !value.ascii_only?
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# Returns [IOStreams::Row::HeaderRow] the columns of a header row that #read_row yields, or nil when there are none,
|
|
355
|
+
# such as for a blank line.
|
|
356
|
+
def as_header_row(columns)
|
|
357
|
+
IOStreams::Row::HeaderRow.new(columns) if columns
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
end
|