diff --git a/Sources/table/Expressions.swift b/Sources/table/Expressions.swift new file mode 100644 index 0000000..e611e36 --- /dev/null +++ b/Sources/table/Expressions.swift @@ -0,0 +1,313 @@ +import Foundation + +// Structure representing a format tree +protocol FormatExpr: CustomStringConvertible { + func fill(row: Row) throws -> String + func validate(header: Header?) throws -> Void +} + +protocol InternalFunction: CustomStringConvertible { + var name: String { get } + func validate(header: Header?, arguments: [any FormatExpr]) throws + func apply(row: Row, arguments: [any FormatExpr]) throws -> String +} + +struct VarExpr: FormatExpr { + let name: String + + init(_ name: String) { + self.name = name + } + + func fill(row: Row) -> String { + return row[name] ?? "" + } + + func validate(header: Header?) throws { + if let h = header { + if h.index(ofColumn: name) == nil { + throw RuntimeError("Unknown column in format: \(name). Supported columns: \(h.columnsStr())") + } + } + } + + var description: String { + return "Var(\(name))" + } +} + +struct TextExpr: FormatExpr { + let text: String + + init(_ text: String) { + self.text = text + } + + func fill(row: Row) -> String { + return text + } + + func validate(header: Header?) throws {} + + var description: String { + return "Text(\(text))" + } +} + +struct FunctionExpr: FormatExpr { + let name: String + let arguments: [any FormatExpr] + static let regex: NSRegularExpression = try! NSRegularExpression(pattern: "\\$\\{([%A-Za-z0-9_\\s]+)\\}") + + init(name: String, arguments: [any FormatExpr] = []) { + self.name = name + self.arguments = arguments + } + + func fill(row: Row) throws -> String { + if let funcDef = Functions.find(name: name) { + return try funcDef.apply(row: row, arguments: arguments) + } else { + throw RuntimeError("Unknown function in format: \(name). Supported functions: \(Functions.names.joined(separator: ", "))") + } + } + + func validate(header: Header?) throws { + if let funcDef = Functions.find(name: name) { + try funcDef.validate(header: header, arguments: arguments) + } else { + throw RuntimeError("Unknown function in format: \(name). Supported functions: \(Functions.names.joined(separator: ", "))") + } + } + + var description: String { + return "Fun(name: \(name), arguments: \(arguments))" + } +} + +struct ExecExpr: FormatExpr { + let command: any FormatExpr + + init(command: any FormatExpr) { + self.command = command + } + + func fill(row: Row) throws -> String { + return try shell(String(describing: command.fill(row: row))) + } + + func validate(header: Header?) throws { + try command.validate(header: header) + } + + var description: String { + return "Exec(command: \(command))" + } +} + +struct FormatGroup: FormatExpr { + let parts: [any FormatExpr] + + init(_ parts: [any FormatExpr]) { + self.parts = parts + } + + func fill(row: Row) throws -> String { + var result = "" + for part in parts { + result += try part.fill(row: row) + } + return result + } + + func validate(header: Header?) throws { + for part in parts { + try part.validate(header: header) + } + } + + var description: String { + return "Group(\(parts))" + } +} + +class Functions { + + nonisolated(unsafe) static let all: [any InternalFunction] = [ + HeaderPrint(), + Values(), + Uuid(), + Random(), + RandomChoice(), + Prefix(), + Array() + ] + + static func find(name: String) -> (any InternalFunction)? { + return all.first { $0.name == name } + } + + static var names: [String] { all.map { $0.name } } + + class HeaderPrint: InternalFunction { + var name: String { "header" } + + func validate(header: Header?, arguments: [any FormatExpr]) throws { + if arguments.count > 0 { throw RuntimeError("header function does not accept any arguments, got \(arguments.count): \(arguments)") } + if header == nil { throw RuntimeError("Header is not defined") } + } + + func apply(row: Row, arguments: [any FormatExpr]) throws -> String { + return row.header!.columnsStr() + } + + var description: String { + return "header() – returns the header columns as a comma-separated string" + } + } + + class Values: InternalFunction { + var name: String { "values" } + + func validate(header: Header?, arguments: [any FormatExpr]) throws { + if arguments.count > 0 { + if !(try arguments[0].fill(row: Row.empty(header: header)).isBoolean) || arguments.count > 1 { + throw RuntimeError("Internal function 'values' only accepts a single boolean argument: `values(true)` to return values as a quoted comma-separated string, or `values(false)` to return an unquoted comma-separated string. Got \(arguments.count): \(arguments)") + } + } + } + + func apply(row: Row, arguments: [any FormatExpr]) throws -> String { + let quoted = arguments.count > 0 ? (try! arguments[0].fill(row: row)).boolValue : false + + if quoted { + return row.components.enumerated().map { (index, cell) in + let v = cell.value + let type = row.header?.type(ofIndex: index) ?? .string + + if type == .boolean || type == .number || v.caseInsensitiveCompare("null") == .orderedSame { + return v + } else { + return "'\(v.replacingOccurrences(of: "'", with: "''"))'" + } + }.joined(separator: ",") + } else { + return row.components.map { $0.value }.joined(separator: ",") + } + } + + var description: String { + return "values(quote) – returns the row values as a comma-separated string. Arguments: optional boolean argument if true quotes the values depending on their type" + } + } + + class Uuid: InternalFunction { + var name: String { "uuid" } + + func validate(header: Header?, arguments: [any FormatExpr]) throws { + if !arguments.isEmpty { + throw RuntimeError("uuid function does not accept any arguments, got \(arguments.count): \(arguments)") + } + } + + func apply(row: Row, arguments: [any FormatExpr]) throws -> String { + return UUID().uuidString + } + + var description: String { + return "uuid() – returns a random UUID string" + } + } + + class Random: InternalFunction { + var name: String { "random" } + + func validate(header: Header?, arguments: [any FormatExpr]) throws { + if arguments.count < 0 { + throw RuntimeError("Function \(name) accepts one or two arguments. It should be either random(to) or random(from, to)") + } + + if arguments.count > 2 { + throw RuntimeError("Function \(name) accepts at most two arguments, got \(arguments.count). It should be either random(to) or random(from, to)") + } + } + + func apply(row: Row, arguments: [any FormatExpr]) throws -> String { + let from = arguments.count > 0 ? try Int(arguments[0].fill(row: row))! : 0 + let to = arguments.count == 2 ? try Int(arguments[1].fill(row: row))! : try Int(arguments[0].fill(row: row))! + return String(Int.random(in: from...to)) + } + + var description: String { + return "random(start, end) – returns a random integer. Arguments: one or two integers, either random(to) which generates random numbers from 0 or random(from, to) which generates random numbers in a given range" + } + } + + class RandomChoice: InternalFunction { + var name: String { "randomChoice" } + + func validate(header: Header?, arguments: [any FormatExpr]) throws { + if arguments.isEmpty { + throw RuntimeError("Function \(name) requires at least one argument") + } + } + + func apply(row: Row, arguments: [any FormatExpr]) throws -> String { + let choices = try arguments.map { try $0.fill(row: row) } + return choices.randomElement() ?? "" + } + + var description: String { + return "randomChoice(arg1,arg2,...) – returns a random element from the provided arguments. Requires a comma-separated list of arguments to choose from" + } + } + + class Prefix: InternalFunction { + var name: String { "prefix" } + + func validate(header: Header?, arguments: [any FormatExpr]) throws { + guard arguments.count == 3 else { + throw RuntimeError("prefix function requires 3 arguments: a string to prefix, a prefix itself and a numeric length, got \(arguments.count): \(arguments)") + } + + guard let _: Int = try Int(arguments[2].fill(row: Row.empty(header: header))) else { + throw RuntimeError("prefix function requires a numeric length argument") + } + } + + func apply(row: Row, arguments: [any FormatExpr]) throws -> String { + let str = try! arguments[0].fill(row: row) + let pref = try! arguments[1].fill(row: row) + let len = try! Int(arguments[2].fill(row: row))! + + return len-str.count > 0 ? String(repeating: pref, count: len-str.count) + str : str + } + + var description: String { + return "prefix(str, prefix, num) – returns a string prefixed with a given prefix to a given length. Requires three arguments: the string to prefix, the prefix and the length. Example prefix(hello, ,10) returns ' hello" + } + } + + class Array: InternalFunction { + var name: String { "array" } + + func validate(header: Header?, arguments: [any FormatExpr]) throws { + if arguments.isEmpty { + throw RuntimeError("Function \(name) requires at least one argument") + } + } + + func apply(row: Row, arguments: [any FormatExpr]) throws -> String { + let arguments = try arguments.map { try $0.fill(row: row) } + let elements = arguments.count > 1 ? arguments : arguments[0].split(separator: Character(",")).map { String($0).trimmingCharacters(in: .whitespaces) } + + let quoted = !elements.allSatisfy { $0.isNumber || $0.isBoolean || $0.caseInsensitiveCompare("null") == .orderedSame } + + return "[" + elements.map { quoted ? "'\($0)'" : $0 }.joined(separator: ", ") + "]" + } + + var description: String { + return "array(str) – returns a Cassandra representation of an array with the provided elements. Requires a comma-separated list of arguments or at least a single argument that will be split by commas" + } + } +} \ No newline at end of file diff --git a/Sources/table/Extensions.swift b/Sources/table/Extensions.swift index caa55ca..60506f6 100644 --- a/Sources/table/Extensions.swift +++ b/Sources/table/Extensions.swift @@ -32,6 +32,10 @@ extension String { var isBoolean: Bool { return self.caseInsensitiveCompare("true") == .orderedSame || self.caseInsensitiveCompare("false") == .orderedSame } + + var boolValue: Bool { + return self.caseInsensitiveCompare("true") == .orderedSame + } } extension Array { diff --git a/Sources/table/Format.swift b/Sources/table/Format.swift index 32df12a..792f7a4 100644 --- a/Sources/table/Format.swift +++ b/Sources/table/Format.swift @@ -1,191 +1,5 @@ import Foundation -// Structure representing a format tree -protocol FormatExpr: CustomStringConvertible { - func fill(row: Row) throws -> String - func validate(header: Header?) throws -> Void -} - -struct VarPart: FormatExpr { - let name: String - - init(_ name: String) { - self.name = name - } - - func fill(row: Row) -> String { - return row[name] ?? "" - } - - func validate(header: Header?) throws { - if let h = header { - if h.index(ofColumn: name) == nil { - throw RuntimeError("Unknown column in format: \(name). Supported columns: \(h.columnsStr())") - } - } - } - - var description: String { - return "Var(\(name))" - } -} - -struct TextPart: FormatExpr { - let text: String - - init(_ text: String) { - self.text = text - } - - func fill(row: Row) -> String { - return text - } - - func validate(header: Header?) throws {} - - var description: String { - return "Text(\(text))" - } -} - -struct FunctionPart: FormatExpr { - let name: String - let arguments: [any FormatExpr] - - static let internalFunctions = ["header", "values", "quoted_values", "uuid", "random", "randomChoice"] - static let regex: NSRegularExpression = try! NSRegularExpression(pattern: "\\$\\{([%A-Za-z0-9_\\s]+)\\}") - - init(name: String, arguments: [any FormatExpr] = []) { - self.name = name - self.arguments = arguments - } - - func fill(row: Row) throws -> String { - if name == "header" { - guard let header = row.header else { - throw RuntimeError("Header is not defined") - } - return header.columnsStr() - } - - if name == "values" { - return row.components.map({ $0.value }).joined(separator: ",") - } - - if name == "uuid" { - return UUID().uuidString - } - - if name == "random" { - let from = arguments.count > 0 ? try Int(arguments[0].fill(row: row))! : 0 - let to = arguments.count == 2 ? try Int(arguments[1].fill(row: row))! : try Int(arguments[0].fill(row: row))! - return String(Int.random(in: from...to)) - } - - if name == "randomChoice" { - if arguments.isEmpty { - throw RuntimeError("randomChoice function requires at least one argument") - } - let choices = try arguments.map { try $0.fill(row: row) } - return choices.randomElement() ?? "" - } - - if name == "quoted_values" { - return row.components.enumerated().map { (index, cell) in - let v = cell.value - let type = row.header?.type(ofIndex: index) ?? .string - - if type == .boolean || type == .number || v.caseInsensitiveCompare("null") == .orderedSame { - return v - } else { - return "'\(v)'" - } - }.joined(separator: ",") - } - - throw RuntimeError("Unknown function: \(name). Supported functions: \(FunctionPart.internalFunctions.joined(separator: ", "))") - } - - func validate(header: Header?) throws { - if let h = header { - if !FunctionPart.internalFunctions.contains(name) && h.index(ofColumn: name) == nil { - throw RuntimeError("Unknown function in format: \(name). Supported functions: \(FunctionPart.internalFunctions.joined(separator: ", "))") - } - - if (name == "random") { - if arguments.count < 0 { - throw RuntimeError("Function \(name) accepts one or two arguments. It should be either random(to) or random(from, to)") - } - - if arguments.count > 2 { - throw RuntimeError("Function \(name) accepts at most two arguments, got \(arguments.count). It should be either random(to) or random(from, to)") - } - } - - if (name == "randomChoice") { - if arguments.isEmpty { throw RuntimeError("Function \(name) requires at least one argument") } - } - } - } - - var description: String { - return "Fun(name: \(name), arguments: \(arguments))" - } -} - -struct FormatGroup: FormatExpr { - let parts: [any FormatExpr] - - init(_ parts: [any FormatExpr]) { - self.parts = parts - } - - func fill(row: Row) throws -> String { - var result = "" - for part in parts { - result += try part.fill(row: row) - } - return result - } - - func validate(header: Header?) throws { - for part in parts { - try part.validate(header: header) - } - } - - var description: String { - return "Group(\(parts))" - } - - static func == (lhs: FormatGroup, rhs: FormatGroup) -> Bool { - return lhs.parts.map { $0.description } == rhs.parts.map { $0.description } - } -} - -struct ExecPart: FormatExpr { - let command: any FormatExpr - - init(command: any FormatExpr) { - self.command = command - } - - func fill(row: Row) throws -> String { - return try shell(String(describing: command.fill(row: row))) - } - - func validate(header: Header?) throws { - try command.validate(header: header) - } - - var description: String { - return "Exec(command: \(command))" - } - - static func == (lhs: ExecPart, rhs: ExecPart) -> Bool { - return lhs.command.description == rhs.command.description - } -} class Format { let original: String @@ -221,31 +35,31 @@ class Format { let char = input[index] if terminators.contains(char) { - if !buffer.isEmpty { nodes.append(TextPart(buffer)); buffer = "" } + if !buffer.isEmpty { nodes.append(TextExpr(buffer)); buffer = "" } return (nodes, input.index(after: index)) } if input[index...].hasPrefix("${") { - if !buffer.isEmpty { nodes.append(TextPart(buffer)); buffer = "" } + if !buffer.isEmpty { nodes.append(TextExpr(buffer)); buffer = "" } index = input.index(index, offsetBy: 2) let (name, newIndex) = readUntil(input, from: index, delimiter: "}") if let name = name { - nodes.append(VarPart(name)) + nodes.append(VarExpr(name)) } index = newIndex } else if input[index...].hasPrefix("%{") { - if !buffer.isEmpty { nodes.append(TextPart(buffer)); buffer = "" } + if !buffer.isEmpty { nodes.append(TextExpr(buffer)); buffer = "" } index = input.index(index, offsetBy: 2) let (funcNode, newIndex) = parseFunction(input, from: index) nodes.append(funcNode) index = newIndex } else if input[index...].hasPrefix("#{") { - if !buffer.isEmpty { nodes.append(TextPart(buffer)); buffer = "" } + if !buffer.isEmpty { nodes.append(TextExpr(buffer)); buffer = "" } index = input.index(index, offsetBy: 2) let (inner, newIndex) = parse(input, from: index, until: ["}"]) - nodes.append(ExecPart(command: FormatGroup(inner))) + nodes.append(ExecExpr(command: FormatGroup(inner))) index = newIndex } else { @@ -255,7 +69,7 @@ class Format { } if !buffer.isEmpty { - nodes.append(TextPart(buffer)) + nodes.append(TextExpr(buffer)) } return (nodes, index) @@ -302,7 +116,7 @@ class Format { fatalError("Expected closing } for function") } - return (FunctionPart(name: name, arguments: args), input.index(after: index)) + return (FunctionExpr(name: name, arguments: args), input.index(after: index)) } private static func readUntil(_ input: String, from start: String.Index, delimiter: Character) -> (String?, String.Index) { diff --git a/Sources/table/Join.swift b/Sources/table/Join.swift index 1435c1d..747df38 100644 --- a/Sources/table/Join.swift +++ b/Sources/table/Join.swift @@ -28,8 +28,8 @@ class Join { func load() throws -> Join { var duplicates = Set() - - for r in matchTable { + + while let r = try matchTable.next() { let colValue = r[secondColIndex] if rowsCache[colValue] != nil { diff --git a/Sources/table/MainApp.swift b/Sources/table/MainApp.swift index 8c81e7d..45132c8 100644 --- a/Sources/table/MainApp.swift +++ b/Sources/table/MainApp.swift @@ -4,15 +4,18 @@ import Foundation struct Debug { // this one is set on start, so we don't care about concurrency checks nonisolated(unsafe) private static var debug: Bool = false + private static let standardError = FileHandle.standardError + private static let nl = "\n".data(using: .utf8)! static func enableDebug() { debug = true - print("Debug mode enabled") + Debug.debug("Debug mode enabled") } static func debug(_ message: String) { if debug { - print(message) + standardError.write(message.data(using: .utf8)!) + standardError.write(nl) } } @@ -47,7 +50,8 @@ func buildPrinter(formatOpt: Format?, outFileFmt: FileType, outputFile: String?) } @main -struct MainApp: ParsableCommand { +@available(macOS 10.15, macCatalyst 13, iOS 13, tvOS 13, watchOS 6, *) +struct MainApp: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: "table", abstract: "A utility for transforming CSV files of SQL output.", @@ -58,7 +62,13 @@ struct MainApp: ParsableCommand { table in.csv --print "${name} ${last_name}: ${email}" Filter rows and display only specified columns: - table in.csv --filter 'available>5' --columns 'item,available' + table in.csv --filter 'available>5' --columns 'item,available'. + + Some options like --add or --print supports expressions that can be used to substitute column values or execute commands: + - ${column_name} - substitutes column value. Example: ${name} will be substituted with the value of the 'name' column. + - #{command} - executes bash command and substitutes its output. Example: #{echo "hello ${name}"} + - %{function} - executes internal functions. Supported functions: + \(Functions.all.map { "\($0.description)" }.joined(separator: "\n\t")) """, version: appVersion ) @@ -104,7 +114,12 @@ struct MainApp: ParsableCommand { @Option(name: [.customShort("f"), .customLong("filter")], help: "Filter rows by a single value criteria. Example: country=UA or size>10. Supported comparison operations: '=' - equal,'!=' - not equal, < - smaller, <= - smaller or equal, > - bigger, >= - bigger or equal, '^=' - starts with, '$=' - ends with, '~=' - contains.") var filters: [String] = [] - @Option(name: .customLong("add"), help: "Adds a new column from a shell command output allowing to substitute other column values into it. Expressions ${name} and #{cmd} are substituted by column value and command result respectively. Example: --add 'col_name=#{curl http://email-db.com/${email}}'.") + @Option( + name: .customLong("add"), + help: ArgumentHelp( + "Adds a new column from a shell command output allowing to substitute other column values into it", + discussion: "Example: --add 'col_name=#{curl http://email-db.com/${email}}'") + ) var addColumns: [String] = [] @Option(name: .customLong("distinct"), help: "Returns only distinct values for the specified column set. Example: --distinct name,city_id.") @@ -125,7 +140,7 @@ struct MainApp: ParsableCommand { @Option(name: .customLong("generate"), help: "Generates a sample empty table with the specified number of rows. Example: '--generate 1000 --add id=%{uuid}' will generate a table of UUIDs with 1000 rows.") var generate: Int? - mutating func run() throws { + mutating func run() async throws { if debugEnabled { Debug.enableDebug() @@ -207,7 +222,7 @@ struct MainApp: ParsableCommand { var skip = skipLines ?? 0 var limit = limitLines ?? Int.max - while let row = table.next() { + while let row = try table.next() { if let parsedFilters { if (!parsedFilters.allSatisfy { $0.apply(row: row) }) { continue diff --git a/Sources/table/Row.swift b/Sources/table/Row.swift index 4a86315..109db53 100644 --- a/Sources/table/Row.swift +++ b/Sources/table/Row.swift @@ -32,12 +32,21 @@ class Row { self.components = cells } + static func empty(header: Header?) -> Row { + let h = header ?? Header.auto(size: 0) + return Row(header: h, index: 0, components: h.components().map { _ in "" } ) + } + subscript(index: Int) -> String { components[index].value } - subscript(columnName: String) -> String? { + subscript(columnName: String) -> String? { if let index = header?.index(ofColumn: columnName) { + if index >= components.count || index < 0 { + debug("Not found index: \(index) for column \(columnName) in row \(self.index) components, components: \(components). Header: \(header?.components() ?? [])") + return nil + } return components[index].value } else { return nil diff --git a/Sources/table/Table.swift b/Sources/table/Table.swift index 4487948..24e8d9e 100644 --- a/Sources/table/Table.swift +++ b/Sources/table/Table.swift @@ -1,8 +1,8 @@ import Foundation -protocol Table: Sequence, IteratorProtocol { +protocol Table { var header: Header { get } - mutating func next() -> Row? + mutating func next() throws -> Row? } protocol InMemoryTable: Table { @@ -55,7 +55,7 @@ class ParsedTable: Table { } } - func next() -> Row? { + func next() throws -> Row? { line += 1 var row = nextLine() @@ -67,6 +67,10 @@ class ParsedTable: Table { return try! row.map { row in let components = try ParsedTable.readRowComponents(row, type: conf.type, delimeter: conf.delimeter, trim: conf.trim) + if (components.count != conf.header.size) { + debug("WARN: Row \(line) has \(components.count) components, but header has \(conf.header.size) columns. Row:\n'\(row)'") + } + return Row( header: conf.header, index:line, diff --git a/Sources/table/TableView.swift b/Sources/table/TableView.swift index 47c71ff..23941b7 100644 --- a/Sources/table/TableView.swift +++ b/Sources/table/TableView.swift @@ -16,8 +16,8 @@ class JoinTableView: Table { self.join = join } - func next() -> Row? { - let row = table.next() + func next() throws -> Row? { + let row = try table.next() if let row { let joinedRow = join.matching(row: row) let joinedColumns = joinedRow.map{ $0.components } ?? [Cell](repeating: Cell(value: ""), count: self.join.matchTable.header.size) @@ -49,8 +49,8 @@ class NewColumnsTableView: Table { self.additionalColumns = additionalColumns } - func next() -> Row? { - let row = table.next() + func next() throws -> Row? { + let row = try table.next() if let row { let newColumnsData = additionalColumns.map { (_, fmt) in @@ -83,8 +83,8 @@ class ColumnsTableView: Table { self.header = Header(components: visibleColumns, types: types) } - func next() -> Row? { - let row = table.next() + func next() throws -> Row? { + let row = try table.next() if let row { return Row( @@ -112,8 +112,8 @@ class DistinctTableView: Table { self.header = table.header } - func next() -> Row? { - var row = table.next() + func next() throws -> Row? { + var row = try table.next() while let curRow = row { let values = distinctColumns.map { col in curRow[col] ?? "" } @@ -123,7 +123,7 @@ class DistinctTableView: Table { return curRow } - row = table.next() + row = try table.next() } return nil @@ -142,8 +142,8 @@ class SampledTableView: Table { self.header = table.header } - func next() -> Row? { - var row = table.next() + func next() throws -> Row? { + var row = try table.next() while let curRow = row { let useRow = sample() @@ -152,7 +152,7 @@ class SampledTableView: Table { return curRow } - row = table.next() + row = try table.next() } return nil @@ -180,12 +180,12 @@ class InMemoryTableView: InMemoryTable { self.table = table } - func load() { + func load() throws { if loaded { return } - while let row = table.next() { + while let row = try table.next() { rows.append(row) } @@ -193,7 +193,7 @@ class InMemoryTableView: InMemoryTable { } func sort(expr: Sort) throws -> any Table { - load() + try load() try rows.sort { (row1, row2) in for (col, order) in expr.columns { diff --git a/Tests/table-Tests/TableParserTests.swift b/Tests/table-Tests/TableParserTests.swift index a1d6c21..e050685 100644 --- a/Tests/table-Tests/TableParserTests.swift +++ b/Tests/table-Tests/TableParserTests.swift @@ -7,13 +7,13 @@ class TableParserTests: XCTestCase { func testParseEmptyTable() throws { let emptyTable = try ParsedTable.parse(reader: ArrayLineReader(lines: []), hasHeader: nil, headerOverride: nil, delimeter: nil) XCTAssertEqual(emptyTable.conf.delimeter, ",") - XCTAssertNil(emptyTable.next()) + XCTAssertNil(try emptyTable.next()) } func testParseOneColumnTable() throws { let oneLineTable = try ParsedTable.parse(reader: ArrayLineReader(lines: [ "1,2,3" ]), hasHeader: nil, headerOverride: nil, delimeter: nil) XCTAssertEqual(oneLineTable.header.columnsStr(), "1,2,3") - XCTAssertNil(oneLineTable.next()) + XCTAssertNil(try oneLineTable.next()) } // TODO: fix in accordance to https://www.rfc-editor.org/rfc/rfc4180 @@ -25,7 +25,7 @@ class TableParserTests: XCTestCase { XCTAssertEqual(table.header.components()[0], "Column,1") XCTAssertEqual(table.header.components()[1], "Column,2") - let row = table.next()! + let row = try table.next()! XCTAssertEqual(row.components[0].value, "Val,1") XCTAssertEqual(row.components[1].value, "Val,2") diff --git a/Tests/table-Tests/TableSamplerTests.swift b/Tests/table-Tests/TableSamplerTests.swift index 7f5beca..8bda4e6 100644 --- a/Tests/table-Tests/TableSamplerTests.swift +++ b/Tests/table-Tests/TableSamplerTests.swift @@ -5,7 +5,7 @@ class TableSamplerTests: XCTestCase { func testSamplerOnEmptyTable() throws { let empty = SampledTableView(table: ParsedTable.empty(), percentage: 50) - XCTAssertNil(empty.next()) + XCTAssertNil(try empty.next()) } func testHalfSamplerOnOneRowTable() throws { @@ -14,7 +14,7 @@ class TableSamplerTests: XCTestCase { percentage: 50 ) - let sampledSize = count(&smallTable) + let sampledSize = try count(&smallTable) XCTAssertEqual(sampledSize >= 400, true) XCTAssertEqual(sampledSize <= 600, true) @@ -26,15 +26,15 @@ class TableSamplerTests: XCTestCase { percentage: 80 ) - let sampledSize = count(&smallTable) + let sampledSize = try count(&smallTable) XCTAssertEqual(sampledSize >= 700, true) XCTAssertEqual(sampledSize <= 900, true) } - func count(_ table: inout any Table) -> Int { + func count(_ table: inout any Table) throws -> Int { var count = 0 - while table.next() != nil { + while try table.next() != nil { count += 1 } return count