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
|
@@ -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
|