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.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/docs/config.md +13 -9
  3. data/docs/copy_files.md +20 -2
  4. data/docs/extensions.md +18 -4
  5. data/docs/formats.md +644 -16
  6. data/docs/path.md +18 -2
  7. data/docs/streams.md +54 -9
  8. data/docs/tutorial.md +14 -2
  9. data/docs/upgrading.md +470 -23
  10. data/lib/io_streams/builder/deprecations.rb +250 -0
  11. data/lib/io_streams/builder.rb +256 -39
  12. data/lib/io_streams/bzip2/reader.rb +18 -2
  13. data/lib/io_streams/bzip2/writer.rb +11 -2
  14. data/lib/io_streams/deprecation/methods.rb +51 -0
  15. data/lib/io_streams/deprecation/once.rb +27 -0
  16. data/lib/io_streams/deprecation/option_names.rb +26 -0
  17. data/lib/io_streams/deprecation/words.rb +42 -0
  18. data/lib/io_streams/deprecation.rb +259 -0
  19. data/lib/io_streams/encode/reader.rb +54 -3
  20. data/lib/io_streams/encode/writer.rb +19 -2
  21. data/lib/io_streams/encode.rb +99 -0
  22. data/lib/io_streams/errors.rb +51 -0
  23. data/lib/io_streams/gzip/reader.rb +11 -0
  24. data/lib/io_streams/io_streams.rb +365 -35
  25. data/lib/io_streams/line/reader.rb +2 -1
  26. data/lib/io_streams/line/utf8_reader.rb +23 -0
  27. data/lib/io_streams/path.rb +125 -35
  28. data/lib/io_streams/paths/file.rb +72 -9
  29. data/lib/io_streams/paths/http.rb +48 -2
  30. data/lib/io_streams/paths/matcher.rb +6 -3
  31. data/lib/io_streams/paths/s3.rb +180 -9
  32. data/lib/io_streams/paths/sftp.rb +47 -3
  33. data/lib/io_streams/paths.rb +23 -0
  34. data/lib/io_streams/pgp/reader.rb +18 -0
  35. data/lib/io_streams/pgp/writer.rb +16 -0
  36. data/lib/io_streams/pgp.rb +16 -2
  37. data/lib/io_streams/reader.rb +44 -0
  38. data/lib/io_streams/record/reader.rb +59 -68
  39. data/lib/io_streams/record/writer.rb +17 -10
  40. data/lib/io_streams/row/header_row.rb +21 -0
  41. data/lib/io_streams/row/reader.rb +52 -15
  42. data/lib/io_streams/row/writer.rb +64 -13
  43. data/lib/io_streams/schema/columns.rb +456 -0
  44. data/lib/io_streams/schema/constraints.rb +99 -0
  45. data/lib/io_streams/schema/field.rb +967 -0
  46. data/lib/io_streams/schema/json_number.rb +56 -0
  47. data/lib/io_streams/schema/keys.rb +78 -0
  48. data/lib/io_streams/schema/pattern.rb +257 -0
  49. data/lib/io_streams/schema/source.rb +31 -0
  50. data/lib/io_streams/schema.rb +616 -0
  51. data/lib/io_streams/stream.rb +836 -67
  52. data/lib/io_streams/symmetric_encryption/reader.rb +8 -0
  53. data/lib/io_streams/tabular/header.rb +29 -0
  54. data/lib/io_streams/tabular/header_mapping.rb +332 -0
  55. data/lib/io_streams/tabular/mapping.rb +165 -0
  56. data/lib/io_streams/tabular/parser/array.rb +5 -0
  57. data/lib/io_streams/tabular/parser/base.rb +24 -0
  58. data/lib/io_streams/tabular/parser/fixed.rb +197 -0
  59. data/lib/io_streams/tabular/parser/fixed_fields.rb +160 -0
  60. data/lib/io_streams/tabular/parser/hash.rb +10 -0
  61. data/lib/io_streams/tabular/parser/json.rb +17 -0
  62. data/lib/io_streams/tabular/parser/json_fields.rb +132 -0
  63. data/lib/io_streams/tabular/schema_mapping.rb +361 -0
  64. data/lib/io_streams/tabular.rb +344 -30
  65. data/lib/io_streams/utils.rb +441 -6
  66. data/lib/io_streams/version.rb +1 -1
  67. data/lib/io_streams/writer.rb +27 -0
  68. data/lib/io_streams/xlsx/cells.rb +127 -0
  69. data/lib/io_streams/xlsx/reader.rb +56 -0
  70. data/lib/io_streams/xlsx/schema_reader.rb +90 -0
  71. data/lib/io_streams/xlsx/v3_writer.rb +16 -0
  72. data/lib/io_streams/zip/reader.rb +7 -0
  73. data/lib/io_streams/zip/writer.rb +24 -2
  74. data/lib/iostreams.rb +21 -17
  75. metadata +28 -2
@@ -5,6 +5,14 @@ module IOStreams
5
5
  %i[buffer_size version]
6
6
  end
7
7
 
8
+ # Returns [Array<Symbol>] the methods that the stream that v3.0 supplies to a reader block, in place of the one that
9
+ # this reader yields, a `Zlib::GzipReader` for a compressed file, otherwise a `SymmetricEncryption::Reader`,
10
+ # answers: those of an IO that read, see `IOStreams::Reader.v3_stream_methods`. The stream of a compressed file
11
+ # also answers #close in v3.0, which the block need not call, since the stream is closed once the block returns.
12
+ def self.v3_stream_methods(_options)
13
+ V3_READ_METHODS
14
+ end
15
+
8
16
  # read from a file/stream using Symmetric Encryption
9
17
  def self.stream(input_stream, **args, &)
10
18
  Utils.load_soft_dependency("symmetric-encryption", ".enc streaming") unless defined?(SymmetricEncryption)
@@ -5,6 +5,18 @@ module IOStreams
5
5
  # Column names that begin with this prefix have been rejected and should be ignored.
6
6
  IGNORE_PREFIX = "__rejected__".freeze
7
7
 
8
+ # The options of a header, see #initialize: the columns, and the restrictions of the columns read, which only
9
+ # apply when reading.
10
+ OPTION_NAMES = %i[columns allowed_columns required_columns skip_unknown].freeze
11
+ READ_OPTION_NAMES = %i[allowed_columns required_columns skip_unknown].freeze
12
+
13
+ # Returns [Array<Symbol>] the names of the supplied options, see #initialize, that describe the columns of a file:
14
+ # those that are not nil, other than `skip_unknown`, which only applies to `allowed_columns`, so that on its own it
15
+ # never changes the columns. Any other option is left to #initialize, which raises for it.
16
+ def self.column_option_names(options)
17
+ (options.compact.keys & OPTION_NAMES) - %i[skip_unknown]
18
+ end
19
+
8
20
  attr_accessor :allowed_columns, :required_columns, :skip_unknown
9
21
  attr_reader :columns
10
22
 
@@ -118,6 +130,23 @@ module IOStreams
118
130
  end
119
131
  end
120
132
 
133
+ # Returns [String|nil] the first key that two of the columns are, such as "a" for the header row `a,a,b`, or for the
134
+ # columns `A` and `a` once cleansed, see #cleanse!, of which a Hash made by #to_hash holds the value of only the
135
+ # last column, or nil when each column is its own key. A blank or rejected column, which #to_hash leaves out, is
136
+ # not a key.
137
+ def repeated_key
138
+ return if columns.nil?
139
+
140
+ keys = {}
141
+ columns.each do |column|
142
+ next if IOStreams::Utils.blank?(column) || column.start_with?(IGNORE_PREFIX)
143
+ return column if keys.key?(column)
144
+
145
+ keys[column] = true
146
+ end
147
+ nil
148
+ end
149
+
121
150
  # Returns [true|false] whether `allowed_columns` or `required_columns` restrict the columns.
122
151
  def restricted?
123
152
  !allowed_columns.nil? || !required_columns.nil?
@@ -0,0 +1,332 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IOStreams
4
+ class Tabular
5
+ # Maps the columns of a file without a schema, see `IOStreams::Tabular::Mapping`, by its header: the columns of its
6
+ # header row, or the columns supplied in place of one, and the column options, `allowed_columns`,
7
+ # `required_columns` and `skip_unknown`, see `IOStreams::Tabular::Header`.
8
+ #
9
+ # It reads and writes the rows and records as IOStreams 2.1 does, whose readers and writers of rows and records
10
+ # held the code of its reading and writing methods, see `IOStreams::Row::Reader` and the others, so that a file
11
+ # without a schema is read and written as before.
12
+ class HeaderMapping < Mapping
13
+ class << self
14
+ # Sets whether the column restrictions, the `allowed_columns`, `required_columns` and `skip_unknown` options of
15
+ # the header, apply to every input: also to the columns supplied, to a header row read with
16
+ # `cleanse_header: false`, and to the keys of each record of a format without a header row, such as JSON, see
17
+ # #restrict_header and #restrict, rather than only to a header row that is read and cleansed. Each mapping reads
18
+ # it once, when it is created, see #initialize.
19
+ #
20
+ # The application sets it with `IOStreams.enforce_column_restrictions=`, which is deprecated, and reports a
21
+ # deprecation warning. The mapping, which applies the setting, holds it, so that IOStreams reads it without one.
22
+ attr_writer :enforce_column_restrictions
23
+
24
+ # Returns [true|false] whether the column restrictions apply to every input, see .enforce_column_restrictions=.
25
+ def enforce_column_restrictions?
26
+ @enforce_column_restrictions
27
+ end
28
+ end
29
+
30
+ @enforce_column_restrictions = false
31
+
32
+ # Parameters:
33
+ # cleanse: [true|false]
34
+ # Whether to cleanse the header row read, see `IOStreams::Tabular::Header#cleanse!`, as the `cleanse_header`
35
+ # option of `IOStreams::Tabular.new` says, which #read_header, #read_row, #read_record_line and
36
+ # #restrict_columns apply, unless #read_header is told otherwise.
37
+ # Default: true
38
+ #
39
+ # See `IOStreams::Tabular::Mapping#initialize` for the others.
40
+ def initialize(header:, parser:, format:, cleanse: true)
41
+ super(header: header, parser: parser, format: format)
42
+ @cleanse = cleanse
43
+ # Whether the columns were supplied, which a writer writes as the header row before the first row or record,
44
+ # see #start_rows.
45
+ @supplied = !header.columns.nil?
46
+ # Whether a reader of records has logged that the column restrictions would change what it reads, which it
47
+ # logs once, see #warn_restriction.
48
+ @warned = false
49
+ # Whether the column restrictions apply to every input, read once, rather than for each record, see
50
+ # .enforce_column_restrictions?.
51
+ @enforced = HeaderMapping.enforce_column_restrictions?
52
+ end
53
+
54
+ # Returns [true|false] whether a header is still required in order to parse or render the current format.
55
+ def header?
56
+ @parser.requires_header? && IOStreams::Utils.blank?(header.columns)
57
+ end
58
+
59
+ # Returns [true|false] whether a header row show be rendered on output.
60
+ def requires_header?
61
+ @parser.requires_header?
62
+ end
63
+
64
+ # Returns [true|false] whether the rows read start with a header row: the header row of a file with one, until it
65
+ # is read, see #header?.
66
+ def rows_header?
67
+ header?
68
+ end
69
+
70
+ # Returns [true|false] whether the rows read are the records of the file, see `IOStreams::Tabular#record_rows?`:
71
+ # those of a format whose parser reads each line into a record, keyed by the names of its values, such as JSON, as
72
+ # its class says, see `IOStreams::Tabular::Parser::Base.keyed?`. A format that an application registers need not
73
+ # have a parser of that class, see `IOStreams::Tabular.register_format`, so one whose class does not say reads
74
+ # rows, as the class does by default.
75
+ def record_rows?
76
+ klass = @parser.class
77
+ klass.respond_to?(:keyed?) && klass.keyed?
78
+ end
79
+
80
+ # Returns [nil] only the fields of a schema are the columns of a file without a header row, see
81
+ # `IOStreams::Tabular#headerless_columns`.
82
+ def headerless_columns
83
+ nil
84
+ end
85
+
86
+ # Returns [String|nil] the key of a record that two columns of the header are, see
87
+ # `IOStreams::Tabular::Header#repeated_key`, of which a record holds the value of only the last column.
88
+ def repeated_key
89
+ header.repeated_key
90
+ end
91
+
92
+ # Returns [Array] the header row/line after parsing.
93
+ # Returns `nil` if the row/line is blank, or a header is not required for the supplied format (:json, :hash).
94
+ #
95
+ # Notes:
96
+ # * Call `header?` first to determine if the header should be parsed first.
97
+ # * The header columns are set after parsing the row, but the header is not cleansed.
98
+ def parse_header(line)
99
+ return if IOStreams::Utils.blank?(line) || !@parser.requires_header?
100
+
101
+ header.columns = @parser.parse(line)
102
+ end
103
+
104
+ # Parses the header row, see #parse_header, then cleanses its columns when `cleanse`, otherwise applies the column
105
+ # restrictions to them, as a reader of records reads its header row, see #read_record_line. `cleanse` defaults to
106
+ # whether to cleanse the header row read, see #initialize.
107
+ #
108
+ # Returns [Array] the header row as read, before it is cleansed, or nil when the line is blank, or the format does
109
+ # not have a header row.
110
+ def read_header(line, cleanse: @cleanse)
111
+ columns = parse_header(line)
112
+ if cleanse
113
+ header.cleanse!(rename: cleanse)
114
+ elsif restricted?
115
+ restrict_header(cleanse)
116
+ end
117
+ columns
118
+ end
119
+
120
+ # Returns [Array] the row/line as a parsed Array of values.
121
+ # Returns nil if the row/line is blank.
122
+ def row_parse(line)
123
+ return if IOStreams::Utils.blank?(line)
124
+
125
+ @parser.parse(line)
126
+ end
127
+
128
+ # Returns [Hash<String,Object>] the line as a hash.
129
+ # Returns nil if the line is blank.
130
+ def record_parse(line)
131
+ line = row_parse(line)
132
+ header.to_hash(line) if line
133
+ end
134
+
135
+ # Renders the output row
136
+ def render(row)
137
+ return if IOStreams::Utils.blank?(row)
138
+
139
+ @parser.render(row, header)
140
+ end
141
+
142
+ # Returns [String] the header rendered for the output format
143
+ # Return nil if no header is required.
144
+ def render_header
145
+ return unless requires_header?
146
+
147
+ if IOStreams::Utils.blank?(header.columns)
148
+ raise(
149
+ Errors::MissingHeader,
150
+ "Header columns must be set before attempting to render a header for format: #{@format.inspect}"
151
+ )
152
+ end
153
+
154
+ @parser.render(header.columns, header)
155
+ end
156
+
157
+ # Yields [Array] each row of the line of a file of rows: the header row as it was parsed, see #parse_header, while
158
+ # it is expected, see #header?, otherwise the row, see #row_parse, or nil for a blank line. The header columns are
159
+ # then cleansed, when the header row read is cleansed, see #initialize, but the header row yielded is left as it is.
160
+ def read_row(line)
161
+ if header?
162
+ columns = parse_header(line)
163
+ header.cleanse! if @cleanse
164
+ yield columns
165
+ else
166
+ yield row_parse(line)
167
+ end
168
+ end
169
+
170
+ # Yields [Hash] the record of the line of a file of records, see #record_parse, or nil for a blank line, unless the
171
+ # line is the header row, while it is expected, see #header?, which it reads instead, see #read_header. The column
172
+ # restrictions are applied to the keys of each record of a format without a header row, such as JSON, when they
173
+ # are enforced, see .enforce_column_restrictions? and #restrict.
174
+ def read_record_line(line)
175
+ if header?
176
+ read_header(line)
177
+ nil
178
+ else
179
+ yield restrict(record_parse(line))
180
+ end
181
+ end
182
+
183
+ # Applies the column restrictions to the columns supplied, which take the place of a header row, as a reader of
184
+ # records does before it reads the file, see `IOStreams::Record::Reader#initialize`, renaming them when the header
185
+ # row read is cleansed, see #initialize.
186
+ def restrict_columns
187
+ restrict_header(@cleanse) if restricted? && !header?
188
+ end
189
+
190
+ # Yields [String] the header line of the columns supplied, which a writer of rows or records writes before the
191
+ # first row or record, see `IOStreams::Row::Writer#initialize`, when the format has a header row.
192
+ def start_rows
193
+ yield render_header if @supplied && requires_header?
194
+ end
195
+
196
+ alias start_records start_rows
197
+
198
+ # Yields [String|nil] the line that a writer of rows writes for the row: the header line, when the columns are not
199
+ # known yet, whose columns it gives, otherwise the line of the row, see #render, or nil for a blank row.
200
+ #
201
+ # Returns the result of the block.
202
+ def render_row(row)
203
+ if header?
204
+ # If header (columns) was not supplied as an argument, assume first line is the header.
205
+ header.columns = row
206
+ yield render_header
207
+ else
208
+ yield render(row)
209
+ end
210
+ end
211
+
212
+ # Yields [String|nil] each line that a writer of records writes for the record: the header line first, once, of
213
+ # the keys of the first record, when the columns are not known yet, then the line of the record, see #render.
214
+ #
215
+ # Returns the result of the block.
216
+ def render_record(record)
217
+ if header?
218
+ # Extract header from the keys from the first row when not supplied above.
219
+ header.columns = record.keys
220
+ yield render_header
221
+ end
222
+ yield render(record)
223
+ end
224
+
225
+ # Yields [String|nil] the line that a writer of rows writes for the header row of the rows after it, such as an
226
+ # `IOStreams::Row::HeaderRow` that `IOStreams::Stream#each(:array)` yields with a schema, or that a copy hands it,
227
+ # see `IOStreams::Tabular#copy_row`: as the first row, see #render_row, unless the format has no header row, such
228
+ # as JSON, and the columns are not known, when it takes them as the columns of the rows after it, without a line.
229
+ # Yields nothing when the columns were supplied, which name the values of the rows instead.
230
+ #
231
+ # Raises `IOStreams::Errors::MissingHeader` when the columns that it takes are empty.
232
+ # Raises `IOStreams::Errors::InvalidHeader` for a format without a header row whose rows are not records, see
233
+ # `IOStreams::Tabular::Parser::Base.keyed?`, such as fixed width, whose layout gives its columns by their keys,
234
+ # which the columns of a header row are not matched to, rather than write blank values. IOStreams 2.1 wrote it
235
+ # as a row, which raised it too.
236
+ def render_header_row(columns, &)
237
+ return if @supplied
238
+ return render_row(columns, &) if requires_header? || !header.columns.nil?
239
+
240
+ unless @parser.class.keyed?
241
+ raise(Errors::InvalidHeader, "The header row of the rows written to the #{@format.inspect} format cannot give " \
242
+ "its columns, which its layout gives by their keys. Describe the columns of the " \
243
+ "file with #schema instead, whose fields the columns of the header row are " \
244
+ "matched to")
245
+ end
246
+ if IOStreams::Utils.blank?(columns)
247
+ raise(Errors::MissingHeader, "The header row of the rows written to the #{@format.inspect} format has no columns")
248
+ end
249
+
250
+ header.columns = columns
251
+ nil
252
+ end
253
+
254
+ # Returns [Array<String>] the header columns once they are cleansed, see `IOStreams::Tabular::Header#cleanse!`.
255
+ def cleanse_header!
256
+ header.cleanse!
257
+ header.columns
258
+ end
259
+
260
+ # Yields nothing: only the fields of a schema are the columns of a file without a header row, see
261
+ # `IOStreams::Tabular#start_copy`.
262
+ def start_copy
263
+ nil
264
+ end
265
+
266
+ # Yields [Array|nil] each row of the line that a copy writes, see `IOStreams::Tabular#copy_row`: as #read_row yields
267
+ # it, but the header row as an `IOStreams::Row::HeaderRow`, which #read_row yields as an Array, as IOStreams 2.1
268
+ # does.
269
+ #
270
+ # Returns the result of the block.
271
+ def copy_row(line, &)
272
+ return read_row(line, &) unless header?
273
+
274
+ read_row(line) { |columns| yield(columns && IOStreams::Row::HeaderRow.new(columns)) }
275
+ end
276
+
277
+ private
278
+
279
+ def restricted?
280
+ header.restricted?
281
+ end
282
+
283
+ # Apply the allowed and required columns to supplied columns, or to a header row read with
284
+ # `cleanse_header: false`, renaming them when `cleanse`. Unless they are enforced, see
285
+ # .enforce_column_restrictions?, only warn when they would change the columns.
286
+ def restrict_header(cleanse)
287
+ return header.cleanse!(rename: cleanse) if @enforced
288
+ return if @warned
289
+
290
+ columns = header.columns
291
+ changed = changed_by_restriction? do
292
+ copy = IOStreams::Tabular::Header.new(
293
+ columns: columns,
294
+ allowed_columns: header.allowed_columns,
295
+ required_columns: header.required_columns,
296
+ skip_unknown: header.skip_unknown
297
+ )
298
+ copy.cleanse!(rename: cleanse)
299
+ copy.columns != columns
300
+ end
301
+ warn_restriction if changed
302
+ end
303
+
304
+ # Formats such as JSON have no header row, so apply the allowed and required columns to each record's keys,
305
+ # renaming them when the header row read is cleansed, see #initialize. Unless they are enforced, see
306
+ # .enforce_column_restrictions?, only warn when they would change the record.
307
+ def restrict(record)
308
+ return record unless record.is_a?(Hash) && restricted? && header.columns.nil?
309
+ return header.restrict_hash(record, rename: @cleanse) if @enforced
310
+ return record if @warned
311
+
312
+ warn_restriction if changed_by_restriction? { header.restrict_hash(record, rename: @cleanse) != record }
313
+ record
314
+ end
315
+
316
+ def changed_by_restriction?
317
+ yield
318
+ rescue IOStreams::Errors::InvalidHeader
319
+ true
320
+ end
321
+
322
+ # Warn once per reader, since the same columns usually apply to every record.
323
+ def warn_restriction
324
+ @warned = true
325
+ IOStreams.logger&.warn(
326
+ "allowed_columns and required_columns are not applied to this input, but would change the records read. " \
327
+ "They are deprecated: describe the columns with #schema instead, which applies to every input."
328
+ )
329
+ end
330
+ end
331
+ end
332
+ end
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IOStreams
4
+ class Tabular
5
+ # Maps the columns of one file to the values of its rows and records, for `IOStreams::Tabular`, which asks it to
6
+ # read and render them rather than depend on what describes the columns. The Tabular chooses one when it is created:
7
+ #
8
+ # * `IOStreams::Tabular::HeaderMapping`, without a schema: by the columns of the header row, or the columns
9
+ # supplied in place of one, and the column options, held by its header, see `IOStreams::Tabular::Header`.
10
+ # * `IOStreams::Tabular::SchemaMapping`, with a schema: by its fields, see `IOStreams::Schema::Columns`.
11
+ #
12
+ # Each one does the same things, in its own way, by the methods below, which each one implements:
13
+ # * Reads the lines of a file: its header row, `#parse_header` and `#read_header`, and its rows and records,
14
+ # `#row_parse` and `#record_parse`, which its parser parses.
15
+ # * Renders the rows and records written into lines, `#render`, and the header row, `#render_header`.
16
+ # * Reads and writes a line at a time for the readers and writers of rows and records, see `IOStreams::Row::Reader`
17
+ # and the others, which only iterate: `#read_row`, `#read_record_line` and `#restrict_columns` when reading,
18
+ # `#start_copy` and `#copy_row` when the rows read are copied, `#start_rows`, `#render_row`, `#start_records` and
19
+ # `#render_record` when writing, and `#render_header_row` for the header row of the rows written, such as that of
20
+ # the file that is copied.
21
+ # * Cleanses the header row, `#cleanse_header!`, when the keys of the records are the columns of the file.
22
+ # * Answers the questions of the protocol: whether the file has a header row, `#requires_header?`, whether it is
23
+ # still to be read, `#header?`, whether the rows read start with a header row, `#rows_header?`, whether they are
24
+ # records rather than Arrays, `#record_rows?`, the columns of the rows of a file without one,
25
+ # `#headerless_columns`, and the key that two columns are, `#repeated_key`.
26
+ #
27
+ # Each reader and writer has its own, through its Tabular, since it holds the columns of one file.
28
+ class Mapping
29
+ # Returns [IOStreams::Tabular::Header] the columns of the file: the header row read, or the columns supplied in
30
+ # place of one, and the column options, see `IOStreams::Tabular#header`.
31
+ attr_reader :header
32
+
33
+ # Parameters:
34
+ # header: [IOStreams::Tabular::Header]
35
+ # The columns of the file, and the column options of the call that reads or writes it.
36
+ #
37
+ # parser: [IOStreams::Tabular::Parser::Base]
38
+ # Parses and renders each line of the file in its format.
39
+ #
40
+ # format: [Symbol]
41
+ # The name of the format, for the messages of errors.
42
+ def initialize(header:, parser:, format:)
43
+ @header = header
44
+ @parser = parser
45
+ @format = format
46
+ end
47
+
48
+ # Returns [true|false] whether the header row is still to be read, see `IOStreams::Tabular#header?`.
49
+ def header?
50
+ raise NotImplementedError
51
+ end
52
+
53
+ # Returns [true|false] whether the file has a header row, see `IOStreams::Tabular#requires_header?`.
54
+ def requires_header?
55
+ raise NotImplementedError
56
+ end
57
+
58
+ # Returns [true|false] whether the rows read start with a header row, see `IOStreams::Tabular#rows_header?`.
59
+ def rows_header?
60
+ raise NotImplementedError
61
+ end
62
+
63
+ # Returns [true|false] whether the rows read are the records of the file, see `IOStreams::Tabular#record_rows?`.
64
+ def record_rows?
65
+ raise NotImplementedError
66
+ end
67
+
68
+ # Returns [Array<String|Symbol>|nil] the columns of the rows of a file without a header row, see
69
+ # `IOStreams::Tabular#headerless_columns`.
70
+ def headerless_columns
71
+ raise NotImplementedError
72
+ end
73
+
74
+ # Returns [String|nil] the key of a record that two columns are, see `IOStreams::Tabular#repeated_key`.
75
+ def repeated_key
76
+ raise NotImplementedError
77
+ end
78
+
79
+ # Returns [Array|nil] the header row parsed from the line, see `IOStreams::Tabular#parse_header`.
80
+ def parse_header(line)
81
+ raise NotImplementedError
82
+ end
83
+
84
+ # Returns [Array|nil] the header row read from the line, see `IOStreams::Tabular#read_header`.
85
+ def read_header(line, cleanse: nil)
86
+ raise NotImplementedError
87
+ end
88
+
89
+ # Returns [Array|nil] the row of the line, see `IOStreams::Tabular#row_parse`.
90
+ def row_parse(line)
91
+ raise NotImplementedError
92
+ end
93
+
94
+ # Returns [Hash|nil] the record of the line, see `IOStreams::Tabular#record_parse`.
95
+ def record_parse(line)
96
+ raise NotImplementedError
97
+ end
98
+
99
+ # Returns [String|Array|Hash|nil] the line of the row or record written, see `IOStreams::Tabular#render`.
100
+ def render(row)
101
+ raise NotImplementedError
102
+ end
103
+
104
+ # Returns [String|nil] the header row written, see `IOStreams::Tabular#render_header`.
105
+ def render_header
106
+ raise NotImplementedError
107
+ end
108
+
109
+ # Yields each row of the line of a file of rows, see `IOStreams::Tabular#read_row`.
110
+ def read_row(line)
111
+ raise NotImplementedError
112
+ end
113
+
114
+ # Yields the record of the line of a file of records, see `IOStreams::Tabular#read_record_line`.
115
+ def read_record_line(line)
116
+ raise NotImplementedError
117
+ end
118
+
119
+ # Applies the column restrictions to the columns supplied, see `IOStreams::Tabular#restrict_columns`.
120
+ def restrict_columns
121
+ raise NotImplementedError
122
+ end
123
+
124
+ # Returns [Array<String>] the header columns once they are cleansed, see `IOStreams::Tabular#cleanse_header!`.
125
+ def cleanse_header!
126
+ raise NotImplementedError
127
+ end
128
+
129
+ # Yields the header row that a copy writes before the first line, see `IOStreams::Tabular#start_copy`.
130
+ def start_copy
131
+ raise NotImplementedError
132
+ end
133
+
134
+ # Yields each row of the line that a copy writes, see `IOStreams::Tabular#copy_row`.
135
+ def copy_row(line)
136
+ raise NotImplementedError
137
+ end
138
+
139
+ # Yields the header line written before the first row, see `IOStreams::Tabular#start_rows`.
140
+ def start_rows
141
+ raise NotImplementedError
142
+ end
143
+
144
+ # Yields the line written for the row, see `IOStreams::Tabular#render_row`.
145
+ def render_row(row)
146
+ raise NotImplementedError
147
+ end
148
+
149
+ # Yields the line written for the header row of the rows after it, see `IOStreams::Tabular#render_header_row`.
150
+ def render_header_row(columns)
151
+ raise NotImplementedError
152
+ end
153
+
154
+ # Yields the header line written before the first record, see `IOStreams::Tabular#start_records`.
155
+ def start_records
156
+ raise NotImplementedError
157
+ end
158
+
159
+ # Yields each line written for the record, see `IOStreams::Tabular#render_record`.
160
+ def render_record(record)
161
+ raise NotImplementedError
162
+ end
163
+ end
164
+ end
165
+ end
@@ -3,6 +3,11 @@ module IOStreams
3
3
  class Tabular
4
4
  module Parser
5
5
  class Array < Base
6
+ # Returns [:typed] each row is a Ruby object, so it holds the values of a schema themselves.
7
+ def self.value_form
8
+ :typed
9
+ end
10
+
6
11
  # Returns Array
7
12
  def parse(row)
8
13
  raise(IOStreams::Errors::TypeMismatch, "Format is :array. Invalid input: #{row.class.name}") unless row.is_a?(::Array)
@@ -12,6 +12,30 @@ module IOStreams
12
12
  nil
13
13
  end
14
14
 
15
+ # Returns [true|false] whether each row of this format is a record, a Hash whose keys name its values, such as a
16
+ # line of JSON, rather than its values in the order of its columns, an Array, such as a line of CSV, so that a
17
+ # schema arranges the values of each row that is written in the form that the parser renders, see
18
+ # `IOStreams::Tabular::SchemaMapping`.
19
+ #
20
+ # Defined at the class level since it is a static property of the format, see `.quote_character`.
21
+ def self.keyed?
22
+ false
23
+ end
24
+
25
+ # Returns [:text|:json|:typed] the form in which this format writes the values of a schema, see
26
+ # `IOStreams::Schema::Field#render`: as text, the default.
27
+ def self.value_form
28
+ :text
29
+ end
30
+
31
+ # Returns [IOStreams::Tabular::Parser::Base] the parser of the lines of this format for the supplied schema, see
32
+ # `IOStreams::Tabular.new`: a parser of this format, whose lines are the same with a schema, which arranges the
33
+ # values of each row itself, see `IOStreams::Tabular::SchemaMapping`. A format whose lines depend on the schema,
34
+ # such as fixed width, whose fields give the positions in the line, returns a parser of its own.
35
+ def self.schema_parser(_schema)
36
+ new
37
+ end
38
+
15
39
  # Returns [true|false] whether a header row is required for this format.
16
40
  def requires_header?
17
41
  true