Skip to content

Commit 7111631

Browse files
committed
CSVError created being shared on CSVReader & CSVWriter
1 parent 26b16eb commit 7111631

File tree

10 files changed

+192
-159
lines changed

10 files changed

+192
-159
lines changed

Sources/Active/Reader/Reader.swift

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public final class CSVReader: IteratorProtocol, Sequence {
3535
/// Creates a reader instance that will be used to parse the given `String`.
3636
/// - parameter string: A `String` containing CSV formatted data.
3737
/// - parameter configuration: Recipe detailing how to parse the CSV data (i.e. encoding, delimiters, etc.).
38-
/// - throws: `CSVReader.Error` exclusively.
38+
/// - throws: `CSVError<CSVReader>` exclusively.
3939
public convenience init(string: String, configuration: Configuration = .init()) throws {
4040
let buffer = ScalarBuffer(reservingCapacity: 8)
4141
let iterator = ScalarIterator(scalarIterator: string.unicodeScalars.makeIterator())
@@ -47,11 +47,11 @@ public final class CSVReader: IteratorProtocol, Sequence {
4747
/// If the configuration's encoding hasn't been set and the input data doesn't contain a Byte Order Marker (BOM), UTF8 is presumed.
4848
/// - parameter data: A data blob containing CSV formatted data.
4949
/// - parameter configuration: Recipe detailing how to parse the CSV data (i.e. encoding, delimiters, etc.).
50-
/// - throws: `CSVReader.Error` exclusively.
50+
/// - throws: `CSVError<CSVReader>` exclusively.
5151
public convenience init(data: Data, configuration: Configuration = .init()) throws {
5252
if configuration.presample, let dataEncoding = configuration.encoding {
5353
// A. If the `presample` configuration has been set and the user has explicitly mark an encoding, then the data can parsed into a string.
54-
guard let string = String(data: data, encoding: dataEncoding) else { throw CSVReader.Error.mismatched(encoding: dataEncoding) }
54+
guard let string = String(data: data, encoding: dataEncoding) else { throw Error.mismatched(encoding: dataEncoding) }
5555
try self.init(string: string, configuration: configuration)
5656
} else {
5757
// B. Otherwise, start parsing byte-by-byte.
@@ -72,14 +72,14 @@ public final class CSVReader: IteratorProtocol, Sequence {
7272
/// If the configuration's encoding hasn't been set and the input data doesn't contain a Byte Order Marker (BOM), UTF8 is presumed.
7373
/// - parameter fileURL: The URL indicating the location of the file to be parsed.
7474
/// - parameter configuration: Recipe detailing how to parse the CSV data (i.e. encoding, delimiters, etc.).
75-
/// - throws: `CSVReader.Error` exclusively.
75+
/// - throws: `CSVError<CSVReader>` exclusively.
7676
public convenience init(fileURL: URL, configuration: Configuration = .init()) throws {
7777
if configuration.presample {
7878
// A. If the `presample` configuration has been set, the file can be completely load into memory.
7979
try self.init(data: try Data(contentsOf: fileURL), configuration: configuration); return
8080
} else {
8181
// B. Otherwise, create an input stream and start parsing byte-by-byte.
82-
guard let stream = InputStream(url: fileURL) else { throw CSVReader.Error.invalidFile(url: fileURL) }
82+
guard let stream = InputStream(url: fileURL) else { throw Error.invalidFile(url: fileURL) }
8383
// B.1. Open the stream for usage.
8484
assert(stream.streamStatus == .notOpen)
8585
stream.open()
@@ -107,7 +107,7 @@ public final class CSVReader: IteratorProtocol, Sequence {
107107
/// - parameter configuration: Recipe detailing how to parse the CSV data (i.e. encoding, delimiters, etc.).
108108
/// - parameter buffer: A buffer storing in-flight `Unicode.Scalar`s.
109109
/// - parameter iterator: An iterator providing the CSV `Unicode.Scalar`s.
110-
/// - throws: `CSVReader.Error` exclusively.
110+
/// - throws: `CSVError<CSVReader>` exclusively.
111111
private init(configuration: Configuration, buffer: ScalarBuffer, iterator: ScalarIterator) throws {
112112
self.configuration = configuration
113113
self.settings = try Settings(configuration: configuration, iterator: iterator, buffer: buffer)
@@ -123,7 +123,7 @@ public final class CSVReader: IteratorProtocol, Sequence {
123123
case .none: break
124124
case .firstLine:
125125
guard let headers = try self.parseLine(rowIndex: 0) else { self.status = .finished; return }
126-
guard !headers.isEmpty else { throw CSVReader.Error.invalidEmptyHeader() }
126+
guard !headers.isEmpty else { throw Error.invalidEmptyHeader() }
127127
self.headers = headers
128128
self.count = (rows: 1, fields: headers.count)
129129
// case .unknown:
@@ -143,7 +143,7 @@ extension CSVReader {
143143
/// Parses a CSV row and wraps it in a convenience structure giving accesses to fields through header titles/names.
144144
///
145145
/// Since CSV parsing is sequential, if a previous call of this function encountered an error, subsequent calls will throw the same error.
146-
/// - throws: `CSVReader.Error` exclusively.
146+
/// - throws: `CSVError<CSVReader>` exclusively.
147147
/// - returns: A record structure or `nil` if there isn't anything else to parse. If a record is returned there shall always be at least one field.
148148
/// - seealso: parseRow()
149149
public func parseRecord() throws -> Record? {
@@ -163,7 +163,7 @@ extension CSVReader {
163163
/// Parses a CSV row.
164164
///
165165
/// Since CSV parsing is sequential, if a previous call of this function encountered an error, subsequent calls will throw the same error.
166-
/// - throws: `CSVReader.Error` exclusively.
166+
/// - throws: `CSVError<CSVReader>` exclusively.
167167
/// - returns: The row's fields or `nil` if there isn't anything else to parse. The row will never be an empty array.
168168
public func parseRow() throws -> [String]? {
169169
switch self.status {
@@ -176,7 +176,7 @@ extension CSVReader {
176176
do {
177177
result = try self.parseLine(rowIndex: self.count.rows)
178178
} catch let error {
179-
let e = error as! CSVReader.Error
179+
let e = error as! CSVError<CSVReader>
180180
self.status = .failed(e)
181181
throw e
182182
}
@@ -187,7 +187,7 @@ extension CSVReader {
187187
}
188188

189189
if self.count.rows > 0 {
190-
guard self.count.fields == numFields else { throw CSVReader.Error.invalidFieldCount(rowIndex: self.count.rows+1, parsed: numFields, expected: self.count.fields) }
190+
guard self.count.fields == numFields else { throw Error.invalidFieldCount(rowIndex: self.count.rows+1, parsed: numFields, expected: self.count.fields) }
191191
} else {
192192
self.count.fields = numFields
193193
}
@@ -204,14 +204,14 @@ extension CSVReader {
204204
for (index, header) in self.headers.enumerated() {
205205
let hash = header.hashValue
206206
guard case .none = result.updateValue(index, forKey: hash) else {
207-
throw CSVReader.Error.invalidHashableHeader()
207+
throw Error.invalidHashableHeader()
208208
}
209209
}
210210
return result
211211
}
212212

213213
/// Parses a CSV row.
214-
/// - throws: `CSVReader.Error.invalidInput` exclusively.
214+
/// - throws: `CSVError<CSVReader>` exclusively.
215215
/// - returns: The row's fields or `nil` if there isn't anything else to parse. The row will never be an empty array.
216216
private func parseLine(rowIndex: Int) throws -> [String]? {
217217
var result: [String] = []
@@ -259,7 +259,7 @@ extension CSVReader {
259259
/// Parses the awaiting unicode scalars expecting to form a "unescaped field".
260260
/// - parameter starting: The first regular scalar in the unescaped field.
261261
/// - parameter rowIndex: The index of the row being parsed.
262-
/// - throws: `CSVReader.Error.invalidInput` exclusively.
262+
/// - throws: `CSVError<CSVReader>` exclusively.
263263
/// - returns: The parsed field and whether the row/file ending characters have been found.
264264
private func parseUnescapedField(starting: Unicode.Scalar, rowIndex: Int) throws -> (value: String, isAtEnd: Bool) {
265265
var field: String.UnicodeScalarView = .init(repeating: starting, count: 1)
@@ -270,7 +270,7 @@ extension CSVReader {
270270
guard let scalar = try self.buffer.next() ?? self.iterator.next() else { reachedRowsEnd = true; break fieldLoop }
271271
// There cannot be double quotes on unescaped fields. If one is encountered, an error is thrown.
272272
if scalar == self.settings.escapingScalar {
273-
throw CSVReader.Error.invalidUnescapedField(rowIndex: rowIndex)
273+
throw Error.invalidUnescapedField(rowIndex: rowIndex)
274274
// If the field delimiter is encountered, return the already parsed characters.
275275
} else if try self.isFieldDelimiter(scalar) {
276276
reachedRowsEnd = false
@@ -298,15 +298,15 @@ extension CSVReader {
298298
///
299299
/// When this function is executed, the quote opening the "escaped field" has already been read.
300300
/// - parameter rowIndex: The index of the row being parsed.
301-
/// - throws: `CSVReader.Error` exclusively.
301+
/// - throws: `CSVError<CSVReader>` exclusively.
302302
/// - returns: The parsed field and whether the row/file ending characters have been found.
303303
private func parseEscapedField(rowIndex: Int) throws -> (value: String, isAtEnd: Bool) {
304304
var field: String.UnicodeScalarView = .init()
305305
var reachedRowsEnd = false
306306

307307
fieldLoop: while true {
308308
// 1. Retrieve an scalar (if not there, it means EOF). This case is not allowed without closing the escaping field first.
309-
guard let scalar = try self.buffer.next() ?? self.iterator.next() else { throw CSVReader.Error.invalidEOF(rowIndex: rowIndex) }
309+
guard let scalar = try self.buffer.next() ?? self.iterator.next() else { throw Error.invalidEOF(rowIndex: rowIndex) }
310310
// 2. If the retrieved scalar is not a quote (i.e. "), just store it and continue parsing.
311311
guard scalar == self.settings.escapingScalar else { field.append(scalar); continue fieldLoop }
312312
// 3. If the retrieved scalar was a quote, retrieve the following scalar and check if it is EOF. If so, the field has finished and also the row and the file.
@@ -331,7 +331,7 @@ extension CSVReader {
331331
reachedRowsEnd = true
332332
break
333333
} else {
334-
throw CSVReader.Error.invalidEscapedField(rowIndex: rowIndex)
334+
throw Error.invalidEscapedField(rowIndex: rowIndex)
335335
}
336336
}
337337

@@ -342,27 +342,28 @@ extension CSVReader {
342342
fileprivate extension CSVReader.Error {
343343
/// The given `String.Encoding` is not yet supported by the library.
344344
/// - parameter encoding: The desired byte representatoion.
345-
static func mismatched(encoding: String.Encoding) -> CSVReader.Error {
345+
static func mismatched(encoding: String.Encoding) -> CSVError<CSVReader> {
346346
.init(.invalidConfiguration,
347347
reason: "The data blob didn't match the given string encoding.",
348348
help: "Let the reader infer the encoding or make sure the data blob is correctly formatted.",
349349
userInfo: ["Encoding": encoding])
350350
}
351351
/// Error raised when an input stream cannot be created to the indicated file URL.
352-
static func invalidFile(url: URL) -> CSVReader.Error {
352+
/// - parameter url: The URL address of the invalid file.
353+
static func invalidFile(url: URL) -> CSVError<CSVReader> {
353354
.init(.streamFailure,
354355
reason: "Creating an input stream to the given file URL failed.",
355356
help: "Make sure the URL is valid and you are allowed to access the file. Alternatively set the configuration's presample or load the file in a data blob and use the reader's data initializer.",
356357
userInfo: ["File URL": url])
357358
}
358359
/// Error raised when a header was required, but the line was empty.
359-
static func invalidEmptyHeader() -> CSVReader.Error {
360+
static func invalidEmptyHeader() -> CSVError<CSVReader> {
360361
.init(.invalidConfiguration,
361362
reason: "A header line was expected, but an empty line was found instead.",
362363
help: "Make sure there is a header line at the very beginning of the file or mark the configuration as 'no header'.")
363364
}
364365
/// Error raised when a record is fetched, but there are header names which has the same hash value (i.e. they have the same name).
365-
static func invalidHashableHeader() -> CSVReader.Error {
366+
static func invalidHashableHeader() -> CSVError<CSVReader> {
366367
.init(.invalidInput,
367368
reason: "The header row contain two fields with the same value.",
368369
help: "Request a row instead of a record.")
@@ -371,7 +372,7 @@ fileprivate extension CSVReader.Error {
371372
/// - parameter rowIndex: The location of the row which generated the error.
372373
/// - parameter parsed: The number of parsed fields.
373374
/// - parameter expected: The number of fields expected.
374-
static func invalidFieldCount(rowIndex: Int, parsed: Int, expected: Int) -> CSVReader.Error {
375+
static func invalidFieldCount(rowIndex: Int, parsed: Int, expected: Int) -> CSVError<CSVReader> {
375376
.init(.invalidInput,
376377
reason: "The number of fields is not constant between rows.",
377378
help: "Make sure the CSV file has always the same amount of fields per row.",
@@ -381,23 +382,23 @@ fileprivate extension CSVReader.Error {
381382
}
382383
/// Error raised when a unescape field finds a unescape quote within it.
383384
/// - parameter rowIndex: The location of the row which generated the error.
384-
static func invalidUnescapedField(rowIndex: Int) -> CSVReader.Error {
385+
static func invalidUnescapedField(rowIndex: Int) -> CSVError<CSVReader> {
385386
.init(.invalidInput,
386387
reason: "Quotes aren't allowed within fields which don't start with quotes.",
387388
help: "Sandwich the targeted field with quotes and escape the quote within the field.",
388389
userInfo: ["Row index": rowIndex])
389390
}
390391
/// Error raised when an EOF has been received but the last CSV field was not finalized.
391392
/// - parameter rowIndex: The location of the row which generated the error.
392-
static func invalidEOF(rowIndex: Int) -> CSVReader.Error {
393+
static func invalidEOF(rowIndex: Int) -> CSVError<CSVReader> {
393394
.init(.invalidInput,
394395
reason: "The last field is escaped (through quotes) and an EOF (End of File) was encountered before the field was properly closed (with a final quote character).",
395396
help: "End the targeted field with a quote.",
396397
userInfo: ["Row index": rowIndex])
397398
}
398399
/// Error raised when an escaped field hasn't been properly finalized.
399400
/// - parameter rowIndex: The location of the row which generated the error.
400-
static func invalidEscapedField(rowIndex: Int) -> CSVReader.Error {
401+
static func invalidEscapedField(rowIndex: Int) -> CSVError<CSVReader> {
401402
.init(.invalidInput,
402403
reason: "The last field is escaped (through quotes) and an EOF (End of File) was encountered before the field was properly closed (with a final quote character).",
403404
help: "End the targeted field with a quote.",

0 commit comments

Comments
 (0)