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
data/lib/io_streams/tabular.rb
CHANGED
|
@@ -27,20 +27,78 @@ module IOStreams
|
|
|
27
27
|
#
|
|
28
28
|
# tabular.render({"third"=>"3", "first_field"=>"1" })
|
|
29
29
|
# # => "1,,3"
|
|
30
|
+
#
|
|
31
|
+
# With a schema, see `IOStreams::Stream#schema`, the columns of the file are matched to the fields of the schema,
|
|
32
|
+
# whose names are the keys of the records read, and whose types convert the values read and render those written:
|
|
33
|
+
#
|
|
34
|
+
# tabular = Tabular.new(schema: IOStreams::Schema.new(fields: [{name: "id", type: "integer"}, {name: "name"}]))
|
|
35
|
+
# tabular.read_header("ID,Name")
|
|
36
|
+
# # => ["id", "name"]
|
|
37
|
+
#
|
|
38
|
+
# tabular.record_parse("1,Jack")
|
|
39
|
+
# # => {"id"=>1, "name"=>"Jack"}
|
|
30
40
|
class Tabular
|
|
31
41
|
autoload :Header, "io_streams/tabular/header"
|
|
42
|
+
autoload :HeaderMapping, "io_streams/tabular/header_mapping"
|
|
43
|
+
autoload :Mapping, "io_streams/tabular/mapping"
|
|
44
|
+
autoload :SchemaMapping, "io_streams/tabular/schema_mapping"
|
|
32
45
|
|
|
33
46
|
module Parser
|
|
34
47
|
autoload :Array, "io_streams/tabular/parser/array"
|
|
35
48
|
autoload :Base, "io_streams/tabular/parser/base"
|
|
36
49
|
autoload :Csv, "io_streams/tabular/parser/csv"
|
|
37
50
|
autoload :Fixed, "io_streams/tabular/parser/fixed"
|
|
51
|
+
autoload :FixedFields, "io_streams/tabular/parser/fixed_fields"
|
|
38
52
|
autoload :Hash, "io_streams/tabular/parser/hash"
|
|
39
53
|
autoload :Json, "io_streams/tabular/parser/json"
|
|
54
|
+
autoload :JsonFields, "io_streams/tabular/parser/json_fields"
|
|
40
55
|
autoload :Psv, "io_streams/tabular/parser/psv"
|
|
41
56
|
end
|
|
42
57
|
|
|
43
|
-
|
|
58
|
+
# The default of the `cleanse_header` option, see #initialize, which cleanses the header row without a schema, and
|
|
59
|
+
# of the `cleanse:` of `IOStreams::Tabular::SchemaMapping#read_header`. An object of its own, which no caller can
|
|
60
|
+
# supply, so that an option that is supplied, whatever its value, such as `:default`, is told apart from one that is
|
|
61
|
+
# not, since with a schema, whose fields name the keys of the records instead, one that is supplied raises.
|
|
62
|
+
CLEANSE_BY_DEFAULT = Object.new.freeze
|
|
63
|
+
private_constant :CLEANSE_BY_DEFAULT
|
|
64
|
+
|
|
65
|
+
# The options of #initialize that describe the file, rather than its columns: its format, the file name that names
|
|
66
|
+
# the format, and the schema or the layout of its fields. `IOStreams::Stream` sets them from its path, see
|
|
67
|
+
# `IOStreams::Stream#format` and `IOStreams::Stream#schema`.
|
|
68
|
+
FORMAT_OPTION_NAMES = %i[format file_name schema format_options default_format].freeze
|
|
69
|
+
|
|
70
|
+
# The format of a file whose format is neither supplied nor named by its file name, see #initialize.
|
|
71
|
+
DEFAULT_FORMAT = :csv
|
|
72
|
+
private_constant :DEFAULT_FORMAT
|
|
73
|
+
|
|
74
|
+
# Returns [Array<Symbol>] the names of the supplied options of a reader of rows or records, see
|
|
75
|
+
# `IOStreams::Row::Reader`, that are deprecated, since a schema describes the columns of the file instead, see
|
|
76
|
+
# `IOStreams::Schema`: the options of the header, see `IOStreams::Tabular::Header`, and the `cleanse_header` option
|
|
77
|
+
# of the reader, even when they are nil, since IOStreams 3.0 raises ArgumentError for them, whatever their value.
|
|
78
|
+
def self.deprecated_read_option_names(options)
|
|
79
|
+
names = options.keys & Header::OPTION_NAMES
|
|
80
|
+
names << :cleanse_header if options.key?(:cleanse_header)
|
|
81
|
+
names
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Returns [Array<Symbol>] the names of the supplied options of a writer of rows or records, see
|
|
85
|
+
# `IOStreams::Row::Writer`, that are deprecated, since a schema describes the columns of the file instead: the
|
|
86
|
+
# options of the header, even when they are nil, as for a reader, see .deprecated_read_option_names, other than
|
|
87
|
+
# those that only apply when reading a header, see `IOStreams::Tabular::Header::READ_OPTION_NAMES`, which writing
|
|
88
|
+
# does not apply.
|
|
89
|
+
def self.deprecated_write_option_names(options)
|
|
90
|
+
options.keys & (Header::OPTION_NAMES - Header::READ_OPTION_NAMES)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
attr_reader :format, :parser
|
|
94
|
+
|
|
95
|
+
# Returns [IOStreams::Schema|nil] the schema of the rows and records, see #initialize.
|
|
96
|
+
attr_reader :schema
|
|
97
|
+
|
|
98
|
+
# Returns [IOStreams::Tabular::Mapping] the mapping of the columns of the file to the values of its rows and records,
|
|
99
|
+
# by the header row or the columns supplied, or by the fields of a schema, which is chosen when the Tabular is
|
|
100
|
+
# created, see #initialize.
|
|
101
|
+
attr_reader :mapping
|
|
44
102
|
|
|
45
103
|
# Parse a delimited data source.
|
|
46
104
|
#
|
|
@@ -82,79 +140,258 @@ module IOStreams
|
|
|
82
140
|
# then this default format will be used.
|
|
83
141
|
# Default: :csv
|
|
84
142
|
# Set to nil to force it to raise an exception when the format is undefined.
|
|
85
|
-
|
|
86
|
-
|
|
143
|
+
#
|
|
144
|
+
# schema: [IOStreams::Schema]
|
|
145
|
+
# The fields of the file, whose names are the keys of each record read, and the type of each one, which converts
|
|
146
|
+
# the values of each row read, and renders those written, see `IOStreams::Stream#schema`. The columns of the
|
|
147
|
+
# file are matched to the fields, see `IOStreams::Schema::Columns`, and the schema gives the position of each
|
|
148
|
+
# field of a fixed width file.
|
|
149
|
+
# Since the schema describes the columns of the file, the options above that describe them without one,
|
|
150
|
+
# `columns`, `allowed_columns` and `required_columns`, raise ArgumentError with it, unless they are nil, and so
|
|
151
|
+
# do `format_options`, unless they are empty, and `cleanse_header`, see below. `skip_unknown` only applies to
|
|
152
|
+
# `allowed_columns`, so it does not, and has no effect.
|
|
153
|
+
# Optional. Default: nil
|
|
154
|
+
#
|
|
155
|
+
# decoded: [true|false]
|
|
156
|
+
# With a schema, whether the lines read are already in the encoding of their text, as `IOStreams::Stream` reads
|
|
157
|
+
# them, see `IOStreams::Builder#utf8_text?`, so that they are taken as they are. Otherwise a binary line, as a
|
|
158
|
+
# stream reads it without an encoding, is taken as UTF-8, see `IOStreams::Tabular::SchemaMapping`. Raises
|
|
159
|
+
# ArgumentError without a schema, whose lines are taken as they are.
|
|
160
|
+
# Default: false
|
|
161
|
+
#
|
|
162
|
+
# cleanse_header: [true|false]
|
|
163
|
+
# Whether to cleanse the header row read, see `IOStreams::Tabular::Header#cleanse!`, as the `cleanse_header`
|
|
164
|
+
# option of a reader of rows or records says, see `IOStreams::Row::Reader`, which #read_header, #read_row,
|
|
165
|
+
# #read_record_line and #restrict_columns apply, unless #read_header is told otherwise. nil does not cleanse it,
|
|
166
|
+
# as before. It only applies when reading, so a writer of rows or records does not accept it, see
|
|
167
|
+
# .check_write_options. With a schema it raises ArgumentError, even when nil, since the fields of the schema name
|
|
168
|
+
# the keys of the records instead.
|
|
169
|
+
# Default: cleanses it.
|
|
170
|
+
def initialize(format: nil, file_name: nil, format_options: nil, default_format: DEFAULT_FORMAT, schema: nil,
|
|
171
|
+
decoded: nil, cleanse_header: CLEANSE_BY_DEFAULT, **args)
|
|
172
|
+
if schema
|
|
173
|
+
reject_schema_options(format_options, args)
|
|
174
|
+
reject_header_cleansing(cleanse_header)
|
|
175
|
+
elsif !decoded.nil?
|
|
176
|
+
raise(ArgumentError, "decoded: only applies with a schema, without which the lines are taken as they are")
|
|
177
|
+
end
|
|
178
|
+
header = Header.new(**args)
|
|
87
179
|
@format = file_name && format.nil? ? self.class.format_from_file_name(file_name) : format
|
|
88
180
|
@format ||= default_format
|
|
89
181
|
raise(Errors::UnknownFormat, "The format cannot be inferred from the file name: #{file_name}") unless @format
|
|
90
182
|
|
|
91
183
|
klass = self.class.parser_class(@format)
|
|
92
|
-
@
|
|
184
|
+
@schema = schema
|
|
185
|
+
if schema
|
|
186
|
+
unless klass.respond_to?(:schema_parser)
|
|
187
|
+
raise(ArgumentError, "The #{@format.inspect} format cannot be read or written with a schema, since its parser " \
|
|
188
|
+
"is not an IOStreams::Tabular::Parser::Base")
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
@parser = klass.schema_parser(schema)
|
|
192
|
+
@mapping = SchemaMapping.new(schema, header: header, parser: @parser, format: @format, decoded: decoded || false)
|
|
193
|
+
else
|
|
194
|
+
@parser = format_options ? klass.new(**format_options) : klass.new
|
|
195
|
+
@mapping = HeaderMapping.new(header: header, parser: @parser, format: @format, cleanse: cleanse_header ? true : false)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Returns [IOStreams::Tabular::Header] the columns of the file: the header row read, or the columns supplied in
|
|
200
|
+
# place of one, and their restrictions, see `IOStreams::Tabular::Header`. With a schema, the header row as it was
|
|
201
|
+
# read, see #read_header.
|
|
202
|
+
def header
|
|
203
|
+
@mapping.header
|
|
93
204
|
end
|
|
94
205
|
|
|
95
206
|
# Returns [true|false] whether a header is still required in order to parse or render the current format.
|
|
96
207
|
def header?
|
|
97
|
-
|
|
208
|
+
@mapping.header?
|
|
98
209
|
end
|
|
99
210
|
|
|
100
|
-
# Returns [true|false] whether a header row show be rendered on output
|
|
211
|
+
# Returns [true|false] whether a header row show be rendered on output: when the format has one, unless the
|
|
212
|
+
# schema says that the file has none, see `IOStreams::Schema#header?`.
|
|
101
213
|
def requires_header?
|
|
102
|
-
|
|
214
|
+
@mapping.requires_header?
|
|
103
215
|
end
|
|
104
216
|
|
|
105
|
-
# Returns [
|
|
217
|
+
# Returns [true|false] whether the rows read start with a header row, see #read_row, until it is read: the header
|
|
218
|
+
# row of a file with one, see #header?, or with a schema, the names of the fields of a file without one, such as
|
|
219
|
+
# fixed width, JSON, or a schema whose `header` is false.
|
|
220
|
+
def rows_header?
|
|
221
|
+
@mapping.rows_header?
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Returns [true|false] whether the rows read, see #read_row, are the records of the file, Hashes keyed by the names
|
|
225
|
+
# of their values, rather than Arrays: those of a format whose rows are records, see
|
|
226
|
+
# `IOStreams::Tabular::Parser::Base.keyed?`, such as JSON, without a schema. IOStreams v3.0 reads them as Arrays,
|
|
227
|
+
# after a header row of the keys of the first record, as this version does with a schema.
|
|
228
|
+
def record_rows?
|
|
229
|
+
@mapping.record_rows?
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Returns [String|nil] the key of a record that two columns of the file are, such as "a" for a header row of `a,a,b`
|
|
233
|
+
# without a schema, of which each record read, and each row written to a format whose rows are records, such as
|
|
234
|
+
# JSON, holds the value of only the last column, see `IOStreams::Tabular::Header#repeated_key`. Returns nil when each
|
|
235
|
+
# column is its own key, as with a schema, which raises `IOStreams::Errors::InvalidHeader` for two columns of the
|
|
236
|
+
# same field. IOStreams v3.0 raises `IOStreams::Errors::InvalidHeader` without a schema too.
|
|
237
|
+
def repeated_key
|
|
238
|
+
@mapping.repeated_key
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Returns [Array<String|Symbol>] the columns of the rows of a file without a header row, which #read_row yields as
|
|
242
|
+
# their header row, so that a copy can give them to the target, see #start_copy: the names of the fields of a
|
|
243
|
+
# schema, see `IOStreams::Schema#header?`, such as of fixed width or JSON.
|
|
244
|
+
# Returns nil for any other file, such as one whose rows start with a header row of its own, or without a schema.
|
|
245
|
+
def headerless_columns
|
|
246
|
+
@mapping.headerless_columns
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Returns [Array] the header row/line after parsing.
|
|
106
250
|
# Returns `nil` if the row/line is blank, or a header is not required for the supplied format (:json, :hash).
|
|
107
251
|
#
|
|
108
252
|
# Notes:
|
|
109
253
|
# * Call `header?` first to determine if the header should be parsed first.
|
|
110
254
|
# * The header columns are set after parsing the row, but the header is not cleansed.
|
|
255
|
+
# * With a schema, its columns are matched to the fields, see `IOStreams::Schema::Columns#match`.
|
|
111
256
|
def parse_header(line)
|
|
112
|
-
|
|
257
|
+
@mapping.parse_header(line)
|
|
258
|
+
end
|
|
113
259
|
|
|
114
|
-
|
|
260
|
+
# Parses the header row, then cleanses its columns when `cleanse:` is truthy, otherwise applies the column
|
|
261
|
+
# restrictions to them, as a reader of records does, see `IOStreams::Record::Reader`. `cleanse:` defaults to the
|
|
262
|
+
# `cleanse_header` option, see #initialize.
|
|
263
|
+
#
|
|
264
|
+
# With a schema, matches its columns to the fields instead, see `IOStreams::Schema::Columns#match`, so `cleanse:`
|
|
265
|
+
# cannot be supplied, other than nil. The header columns are the header row as it was read, so that another Tabular
|
|
266
|
+
# with the same schema, such as one that parses the rest of the file elsewhere, matches the same columns with
|
|
267
|
+
# `read_header(header.columns)`.
|
|
268
|
+
#
|
|
269
|
+
# Returns [Array] the header row as read, before it is cleansed, or with a schema, the names of the fields of its
|
|
270
|
+
# columns, in the order of the fields.
|
|
271
|
+
# Returns nil when the line is blank, or the format does not have a header row.
|
|
272
|
+
def read_header(line, **)
|
|
273
|
+
@mapping.read_header(line, **)
|
|
115
274
|
end
|
|
116
275
|
|
|
117
276
|
# Returns [Hash<String,Object>] the line as a hash.
|
|
277
|
+
# With a schema, the keys are the names of the fields, and the values are converted to their types, see
|
|
278
|
+
# `IOStreams::Schema::Columns`.
|
|
118
279
|
# Returns nil if the line is blank.
|
|
119
280
|
def record_parse(line)
|
|
120
|
-
|
|
121
|
-
header.to_hash(line) if line
|
|
281
|
+
@mapping.record_parse(line)
|
|
122
282
|
end
|
|
123
283
|
|
|
124
284
|
# Returns [Array] the row/line as a parsed Array of values.
|
|
285
|
+
# With a schema, the values of its fields, in the order of the fields, converted to their types, see
|
|
286
|
+
# `IOStreams::Schema::Columns`.
|
|
125
287
|
# Returns nil if the row/line is blank.
|
|
126
288
|
def row_parse(line)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
parser.parse(line)
|
|
289
|
+
@mapping.row_parse(line)
|
|
130
290
|
end
|
|
131
291
|
|
|
132
292
|
# Renders the output row
|
|
293
|
+
# With a schema, the value of each field is rendered in the form that the format writes, see
|
|
294
|
+
# `IOStreams::Schema::Field#render`, in the order of the fields.
|
|
133
295
|
def render(row)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
parser.render(row, header)
|
|
296
|
+
@mapping.render(row)
|
|
137
297
|
end
|
|
138
298
|
|
|
139
299
|
# Returns [String] the header rendered for the output format
|
|
140
300
|
# Return nil if no header is required.
|
|
301
|
+
# With a schema, the names of its fields.
|
|
141
302
|
def render_header
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if IOStreams::Utils.blank?(header.columns)
|
|
145
|
-
raise(
|
|
146
|
-
Errors::MissingHeader,
|
|
147
|
-
"Header columns must be set before attempting to render a header for format: #{format.inspect}"
|
|
148
|
-
)
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
parser.render(header.columns, header)
|
|
303
|
+
@mapping.render_header
|
|
152
304
|
end
|
|
153
305
|
|
|
154
306
|
# Returns [Array<String>] the cleansed columns
|
|
307
|
+
#
|
|
308
|
+
# With a schema it raises ArgumentError, as `cleanse_header:` does, see #initialize, since the keys of the records
|
|
309
|
+
# are the names of the fields, which the columns of the header row are matched to instead, see #read_header.
|
|
155
310
|
def cleanse_header!
|
|
156
|
-
|
|
157
|
-
|
|
311
|
+
@mapping.cleanse_header!
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Reading and writing a line at a time: a reader or writer of rows or records, see `IOStreams::Row::Reader` and the
|
|
315
|
+
# others, hands each line or value to these, which the mapping of the columns reads or renders, see
|
|
316
|
+
# `IOStreams::Tabular::Mapping`, so that the reader or writer only iterates. The reading methods cleanse the header
|
|
317
|
+
# row as the `cleanse_header` option says, see #initialize.
|
|
318
|
+
|
|
319
|
+
# Yields [Array|nil] each row of the line of a file of rows: the header row as it is parsed, while it is expected,
|
|
320
|
+
# see #header?, otherwise the row, see #row_parse, or nil for a blank line. With a schema, the header row is an
|
|
321
|
+
# `IOStreams::Row::HeaderRow`, and the rows of a file without one start with the names of the fields, see
|
|
322
|
+
# `IOStreams::Tabular::SchemaMapping#read_row`.
|
|
323
|
+
#
|
|
324
|
+
# Returns the result of the block.
|
|
325
|
+
def read_row(line, &)
|
|
326
|
+
@mapping.read_row(line, &)
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# Yields [Hash|nil] the record of the line of a file of records, see #record_parse, or nil for a blank line, unless
|
|
330
|
+
# the line is the header row, while it is expected, see #header?, which it reads instead, see #read_header.
|
|
331
|
+
def read_record_line(line, &)
|
|
332
|
+
@mapping.read_record_line(line, &)
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Applies the column restrictions to the columns supplied in place of a header row, before a reader of records
|
|
336
|
+
# reads the file, see `IOStreams::Record::Reader#initialize`. With a schema no column options apply.
|
|
337
|
+
def restrict_columns
|
|
338
|
+
@mapping.restrict_columns
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# Yields [IOStreams::Row::HeaderRow] the header row that a copy of the rows of the file writes before its first line,
|
|
342
|
+
# see `IOStreams::Row::Reader#copy_to`, so that the target has the columns of the rows even when the file has no
|
|
343
|
+
# lines: with a schema, the names of the fields of a file without a header row, such as fixed width or JSON, see
|
|
344
|
+
# #headerless_columns, which #copy_row then does not yield again. Yields nothing for any other file, whose header row
|
|
345
|
+
# is a line of its own, if it has one.
|
|
346
|
+
def start_copy(&)
|
|
347
|
+
@mapping.start_copy(&)
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# Yields [Array|nil] each row of the line that a copy of the rows of the file writes, see
|
|
351
|
+
# `IOStreams::Row::Reader#copy_to`, as #read_row yields it, or nil for a blank line, but with the header row as an
|
|
352
|
+
# `IOStreams::Row::HeaderRow`, also without a schema, so that the writer takes it as the header row of the rows after
|
|
353
|
+
# it, see `IOStreams::Row::Writer#<<`, rather than as a row of values.
|
|
354
|
+
#
|
|
355
|
+
# Returns the result of the block.
|
|
356
|
+
def copy_row(line, &)
|
|
357
|
+
@mapping.copy_row(line, &)
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# Yields [String] the header line that a writer of rows writes before the first row: the columns supplied, or with
|
|
361
|
+
# a schema, the names of its fields, when the file has a header row.
|
|
362
|
+
def start_rows(&)
|
|
363
|
+
@mapping.start_rows(&)
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# Yields [String|nil] the line that a writer of rows writes for the row. Without a schema, the first row is the
|
|
367
|
+
# header row, unless the columns were supplied.
|
|
368
|
+
#
|
|
369
|
+
# Returns the result of the block.
|
|
370
|
+
def render_row(row, &)
|
|
371
|
+
@mapping.render_row(row, &)
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# Yields [String|nil] the line that a writer of rows writes for the header row of the rows after it, such as the
|
|
375
|
+
# `IOStreams::Row::HeaderRow` that `IOStreams::Stream#each(:array)` yields with a schema, or the header row of the
|
|
376
|
+
# file that is copied, see #copy_row, or the names of the fields of a file without one, see #start_copy. With a
|
|
377
|
+
# schema, it gives the columns of the rows after it, which are matched to the fields, without a line. Without one,
|
|
378
|
+
# it yields nothing when the columns were supplied, which name the values of the rows instead.
|
|
379
|
+
def render_header_row(columns, &)
|
|
380
|
+
@mapping.render_header_row(columns, &)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# Yields [String] the header line that a writer of records writes before the first record: the columns supplied,
|
|
384
|
+
# or with a schema, the names of its fields, when the file has a header row.
|
|
385
|
+
def start_records(&)
|
|
386
|
+
@mapping.start_records(&)
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# Yields [String|nil] each line that a writer of records writes for the record. Without a schema, the header line
|
|
390
|
+
# of the keys of the first record comes first, unless the columns were supplied.
|
|
391
|
+
#
|
|
392
|
+
# Returns the result of the block.
|
|
393
|
+
def render_record(record, &)
|
|
394
|
+
@mapping.render_record(record, &)
|
|
158
395
|
end
|
|
159
396
|
|
|
160
397
|
# Register a format and the parser class for it.
|
|
@@ -193,17 +430,94 @@ module IOStreams
|
|
|
193
430
|
nil
|
|
194
431
|
end
|
|
195
432
|
|
|
433
|
+
# Returns [Symbol|nil] the format that this version reads and writes the rows and records of the file name in, when
|
|
434
|
+
# IOStreams v3.0 reads and writes them in its `:text` format instead, as the text of each line: when the last
|
|
435
|
+
# extension of the file name that names a format is `.text`, which this version does not register, so that it uses
|
|
436
|
+
# the format of an extension before it, see .format_from_file_name, or CSV, the default. Returns nil when v3.0 uses
|
|
437
|
+
# the same format, or when `:text` is registered.
|
|
438
|
+
#
|
|
439
|
+
# The extensions are those that v3.0 takes from the file name: those of its last name, after its first part,
|
|
440
|
+
# compared without case, so that `ids.TEXT` names `:text` too, while a file named `text` names no format.
|
|
441
|
+
def self.text_file_format(file_name)
|
|
442
|
+
return if @formats.include?(:text)
|
|
443
|
+
|
|
444
|
+
name = Utils::PathName.basename(file_name.to_s).b.sub(/\A\.+/, "")
|
|
445
|
+
extensions = name.split(".").drop(1).map(&:downcase)
|
|
446
|
+
last = extensions.reverse_each.find { |extension| extension == "text" || @formats.include?(extension.to_sym) }
|
|
447
|
+
format_from_file_name(file_name) || DEFAULT_FORMAT if last == "text"
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
# Returns [true|false] whether the rows of the registered format are lines of text, such as those of CSV or JSON,
|
|
451
|
+
# rather than Ruby objects, such as those of `:array` and `:hash`, see `Parser::Base.value_form`, so that IOStreams
|
|
452
|
+
# v3.0 converts a copy without a mode between it and another format, see `IOStreams::Builder#copy_format`.
|
|
453
|
+
def self.text_rows?(format)
|
|
454
|
+
parser_class(format).value_form != :typed
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
# Returns [:array|:hash] the mode of a copy that converts the rows of a file in one registered format into a file in
|
|
458
|
+
# another, as IOStreams v3.0 converts them in a copy without a mode, see `IOStreams::Stream#warn_copy_format`:
|
|
459
|
+
# `:hash` when the rows of either one are records, such as those of JSON, see `Parser::Base.keyed?`, since the rows
|
|
460
|
+
# of a file of records are read as records without a schema, and a writer of records takes records, otherwise
|
|
461
|
+
# `:array`, which keeps the columns of the header row as they are, where the keys of records are cleansed.
|
|
462
|
+
def self.conversion_mode(source, target)
|
|
463
|
+
parser_class(source).keyed? || parser_class(target).keyed? ? :hash : :array
|
|
464
|
+
end
|
|
465
|
+
|
|
196
466
|
# Returns the parser class for the registered format.
|
|
197
467
|
def self.parser_class(format)
|
|
198
468
|
@formats[format&.to_sym] ||
|
|
199
469
|
raise(ArgumentError, "Unknown Tabular Format: #{format.inspect}")
|
|
200
470
|
end
|
|
201
471
|
|
|
472
|
+
# Raises ArgumentError for the options of #initialize that a writer of rows or records does not accept, see
|
|
473
|
+
# `IOStreams::Row::Writer#initialize`, as before: `cleanse_header`, which only applies when reading, so that it
|
|
474
|
+
# would have no effect.
|
|
475
|
+
def self.check_write_options(options)
|
|
476
|
+
return unless options.key?(:cleanse_header)
|
|
477
|
+
|
|
478
|
+
raise(ArgumentError, "cleanse_header: only applies when reading the header row, so it cannot be supplied when writing")
|
|
479
|
+
end
|
|
480
|
+
|
|
202
481
|
register_format(:array, IOStreams::Tabular::Parser::Array)
|
|
203
482
|
register_format(:csv, IOStreams::Tabular::Parser::Csv)
|
|
204
483
|
register_format(:fixed, IOStreams::Tabular::Parser::Fixed)
|
|
205
484
|
register_format(:hash, IOStreams::Tabular::Parser::Hash)
|
|
206
485
|
register_format(:json, IOStreams::Tabular::Parser::Json)
|
|
207
486
|
register_format(:psv, IOStreams::Tabular::Parser::Psv)
|
|
487
|
+
|
|
488
|
+
private
|
|
489
|
+
|
|
490
|
+
# Raises ArgumentError for the options that describe the columns of a file without a schema, since the schema
|
|
491
|
+
# describes them, see `IOStreams::Schema`, rather than apply one and ignore the other: the options of the header
|
|
492
|
+
# that are not nil, see `IOStreams::Tabular::Header.column_option_names`, and the layout of a fixed width file in
|
|
493
|
+
# format options that are not empty.
|
|
494
|
+
#
|
|
495
|
+
# An option that is nil is not supplied, and `skip_unknown` only applies to `allowed_columns`, so they are accepted,
|
|
496
|
+
# and have no effect, so that a caller, such as Rocket Job, can supply its options whether or not there is a schema.
|
|
497
|
+
def reject_schema_options(format_options, args)
|
|
498
|
+
unless format_options.nil? || format_options.empty?
|
|
499
|
+
raise(ArgumentError, "Supply either schema: or format_options:, not both")
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
names = Header.column_option_names(args)
|
|
503
|
+
return if names.empty?
|
|
504
|
+
|
|
505
|
+
raise(
|
|
506
|
+
ArgumentError,
|
|
507
|
+
"#{names.map { |name| "#{name}:" }.join(', ')} cannot be supplied with a schema, which describes the columns " \
|
|
508
|
+
"of the file: its fields are the columns, `fieldsMatch` says which other columns the file can have, and " \
|
|
509
|
+
"`constraints: {required: true}` which fields it must have, see IOStreams::Schema"
|
|
510
|
+
)
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
# Raises ArgumentError for `cleanse_header:` with a schema, whose fields name the keys of the records read instead,
|
|
514
|
+
# including nil, which does not cleanse the header row, since it would have no effect.
|
|
515
|
+
def reject_header_cleansing(cleanse_header)
|
|
516
|
+
return if cleanse_header.equal?(CLEANSE_BY_DEFAULT)
|
|
517
|
+
|
|
518
|
+
raise(ArgumentError, "cleanse_header: cannot be supplied with a schema, since the keys of its records are the " \
|
|
519
|
+
"names of its fields, which the columns of the header row are matched to, see " \
|
|
520
|
+
"IOStreams::Schema#field_for")
|
|
521
|
+
end
|
|
208
522
|
end
|
|
209
523
|
end
|