From 85585e48fb0ecad763038fc9dae728f19766a999 Mon Sep 17 00:00:00 2001 From: Thomas Leese Date: Sun, 14 Dec 2025 14:38:51 +0000 Subject: [PATCH 1/2] Make `enumerate` methods accept a callback that throws This updates the `enumerateAsArray` and `enumerateAsDict` methods to accept a row callback function that throws an error. If the function does throw an error it will be propogated to the caller of the `enumerate` method. This is inspired by https://github.com/swiftcsv/SwiftCSV/pull/139 with the review comments addressed to hopefully get this functionality in to a release. Co-Authored-By: "Philip B." <145237+philipbel@users.noreply.github.com> --- CHANGELOG.md | 6 ++++++ SwiftCSV/Parser.swift | 20 +++++++++++--------- SwiftCSV/ParsingState.swift | 10 +++++----- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d15d2df..8cd7555 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ Bugfixes: Other: --> +## Unreleased + +API Changes: + +- Allow `enumerateAsArray` and `enumerateAsDict` to accept a function that throws. + ## 0.10.0 Other: diff --git a/SwiftCSV/Parser.swift b/SwiftCSV/Parser.swift index ac329ff..e6db465 100644 --- a/SwiftCSV/Parser.swift +++ b/SwiftCSV/Parser.swift @@ -21,12 +21,14 @@ extension CSV { /// - rowLimit: Amount of rows to consume, beginning to count at `startAt`. Default value is `nil` to consume /// the whole input string. /// - rowCallback: Array of each row's columnar values, in order. - public func enumerateAsArray(startAt: Int = 0, rowLimit: Int? = nil, _ rowCallback: @escaping ([String]) -> ()) throws { + /// - Throws: `CSVParseError` or any error thrown by `rowCallback` + /// + public func enumerateAsArray(startAt: Int = 0, rowLimit: Int? = nil, _ rowCallback: @escaping ([String]) throws -> ()) throws { try Parser.enumerateAsArray(text: self.text, delimiter: self.delimiter, startAt: startAt, rowLimit: rowLimit, rowCallback: rowCallback) } - public func enumerateAsDict(_ block: @escaping ([String : String]) -> ()) throws { + public func enumerateAsDict(_ block: @escaping ([String : String]) throws -> ()) throws { try Parser.enumerateAsDict(header: self.header, content: self.text, delimiter: self.delimiter, block: block) } @@ -55,12 +57,12 @@ enum Parser { /// - rowLimit: Amount of rows to consume, beginning to count at `startAt`. Default value is `nil` to consume /// the whole input string. /// - rowCallback: Callback invoked for every parsed row between `startAt` and `limitTo` in `text`. - /// - Throws: `CSVParseError` + /// - Throws: `CSVParseError` or any error thrown by `rowCallback` static func enumerateAsArray(text: String, delimiter: CSVDelimiter, startAt offset: Int = 0, rowLimit: Int? = nil, - rowCallback: @escaping ([String]) -> ()) throws { + rowCallback: @escaping ([String]) throws -> ()) throws { let maxRowIndex = rowLimit.flatMap { $0 < 0 ? nil : offset + $0 } var currentIndex = text.startIndex @@ -72,7 +74,7 @@ enum Parser { var rowIndex = 0 - func finishRow() { + func finishRow() throws { defer { rowIndex += 1 fields = [] @@ -81,7 +83,7 @@ enum Parser { guard rowIndex >= offset else { return } fields.append(String(field)) - rowCallback(fields) + try rowCallback(fields) } var state: ParsingState = ParsingState( @@ -118,12 +120,12 @@ enum Parser { } if !fields.isEmpty { - rowCallback(fields) + try rowCallback(fields) } } } - static func enumerateAsDict(header: [String], content: String, delimiter: CSVDelimiter, rowLimit: Int? = nil, block: @escaping ([String : String]) -> ()) throws { + static func enumerateAsDict(header: [String], content: String, delimiter: CSVDelimiter, rowLimit: Int? = nil, block: @escaping ([String : String]) throws -> ()) throws { let enumeratedHeader = header.enumerated() @@ -133,7 +135,7 @@ enum Parser { for (index, head) in enumeratedHeader { dict[head] = index < fields.count ? fields[index] : "" } - block(dict) + try block(dict) } } } diff --git a/SwiftCSV/ParsingState.swift b/SwiftCSV/ParsingState.swift index ed37ce0..77abe4f 100644 --- a/SwiftCSV/ParsingState.swift +++ b/SwiftCSV/ParsingState.swift @@ -20,12 +20,12 @@ struct ParsingState { private(set) var innerQuotes = false let delimiter: Character - let finishRow: () -> Void + let finishRow: () throws -> Void let appendChar: (Character) -> Void let finishField: () -> Void init(delimiter: Character, - finishRow: @escaping () -> Void, + finishRow: @escaping () throws -> Void, appendChar: @escaping (Character) -> Void, finishField: @escaping () -> Void) { @@ -44,7 +44,7 @@ struct ParsingState { } else if char == delimiter { finishField() } else if char.isNewline { - finishRow() + try finishRow() } else if char.isWhitespace { // ignore whitespaces between fields } else { @@ -72,7 +72,7 @@ struct ParsingState { atStart = true parsingField = false innerQuotes = false - finishRow() + try finishRow() } else { appendChar(char) } @@ -91,7 +91,7 @@ struct ParsingState { atStart = true parsingQuotes = false innerQuotes = false - finishRow() + try finishRow() } else if char.isWhitespace { // ignore whitespaces between fields } else { From d81189f843bc5eb4a87f7302e13e37f345f77979 Mon Sep 17 00:00:00 2001 From: Thomas Leese Date: Sun, 14 Dec 2025 20:11:47 +0000 Subject: [PATCH 2/2] Allow other `ParsingState` closures to throw This follows up on the previous commit which made the `finishRow` closure of `ParsingState` allowed to throw an error, by updating the other two closures of the private API to also throw an error. Although this is not as neccessary for the private API, it keeps the closures consistent, and there's no reason why one should throw and the others should not. --- SwiftCSV/ParsingState.swift | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/SwiftCSV/ParsingState.swift b/SwiftCSV/ParsingState.swift index 77abe4f..fc0e671 100644 --- a/SwiftCSV/ParsingState.swift +++ b/SwiftCSV/ParsingState.swift @@ -21,13 +21,13 @@ struct ParsingState { let delimiter: Character let finishRow: () throws -> Void - let appendChar: (Character) -> Void - let finishField: () -> Void + let appendChar: (Character) throws -> Void + let finishField: () throws -> Void init(delimiter: Character, finishRow: @escaping () throws -> Void, - appendChar: @escaping (Character) -> Void, - finishField: @escaping () -> Void) { + appendChar: @escaping (Character) throws -> Void, + finishField: @escaping () throws -> Void) { self.delimiter = delimiter self.finishRow = finishRow @@ -42,7 +42,7 @@ struct ParsingState { atStart = false parsingQuotes = true } else if char == delimiter { - finishField() + try finishField() } else if char.isNewline { try finishRow() } else if char.isWhitespace { @@ -50,12 +50,12 @@ struct ParsingState { } else { parsingField = true atStart = false - appendChar(char) + try appendChar(char) } } else if parsingField { if innerQuotes { if char == "\"" { - appendChar(char) + try appendChar(char) innerQuotes = false } else { throw CSVParseError.quotation(message: "Can't have non-quote here: \(char)") @@ -67,26 +67,26 @@ struct ParsingState { atStart = true parsingField = false innerQuotes = false - finishField() + try finishField() } else if char.isNewline { atStart = true parsingField = false innerQuotes = false try finishRow() } else { - appendChar(char) + try appendChar(char) } } } else if parsingQuotes { if innerQuotes { if char == "\"" { - appendChar(char) + try appendChar(char) innerQuotes = false } else if char == delimiter { atStart = true parsingField = false innerQuotes = false - finishField() + try finishField() } else if char.isNewline { atStart = true parsingQuotes = false @@ -101,7 +101,7 @@ struct ParsingState { if char == "\"" { innerQuotes = true } else { - appendChar(char) + try appendChar(char) } } } else {