Merge 'master' into develop
This commit is contained in:
commit
32668fb167
|
@ -2,4 +2,5 @@
|
||||||
/.build
|
/.build
|
||||||
/Packages
|
/Packages
|
||||||
/*.xcodeproj
|
/*.xcodeproj
|
||||||
/build
|
/build
|
||||||
|
/DerivedData
|
||||||
|
|
|
@ -33,12 +33,19 @@ public final class CSVEncoder {
|
||||||
///
|
///
|
||||||
/// Currently, this decideds how `nil` and `bool` values should be handled.
|
/// Currently, this decideds how `nil` and `bool` values should be handled.
|
||||||
public var encodingOptions: CSVCodingOptions
|
public var encodingOptions: CSVCodingOptions
|
||||||
|
|
||||||
|
|
||||||
|
/// The struct that configures serialization options
|
||||||
|
public var configuration: Config
|
||||||
|
|
||||||
/// Creates a new `CSVEncoder` instance.
|
/// Creates a new `CSVEncoder` instance.
|
||||||
///
|
///
|
||||||
/// - Parameter encodingOptions: The encoding options the use when encoding an object.
|
/// - Parameters:
|
||||||
public init(encodingOptions: CSVCodingOptions = .default) {
|
/// - encodingOptions: The encoding options the use when encoding an object.
|
||||||
|
/// - configuration: The struct that configures serialization options
|
||||||
|
public init(encodingOptions: CSVCodingOptions = .default, configuration: Config = Config.default) {
|
||||||
self.encodingOptions = encodingOptions
|
self.encodingOptions = encodingOptions
|
||||||
|
self.configuration = configuration
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a `CSVSyncEncoder` using the registered encoding options.
|
/// Creates a `CSVSyncEncoder` using the registered encoding options.
|
||||||
|
@ -46,7 +53,7 @@ public final class CSVEncoder {
|
||||||
/// This encoder is for if you have several objects that you want to encode at
|
/// This encoder is for if you have several objects that you want to encode at
|
||||||
/// a single time into a single document.
|
/// a single time into a single document.
|
||||||
public var sync: CSVSyncEncoder {
|
public var sync: CSVSyncEncoder {
|
||||||
return CSVSyncEncoder(encodingOptions: self.encodingOptions)
|
return CSVSyncEncoder(encodingOptions: self.encodingOptions, configuration: self.configuration)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new `CSVAsyncEncoder` using the registered encoding options.
|
/// Creates a new `CSVAsyncEncoder` using the registered encoding options.
|
||||||
|
@ -59,7 +66,7 @@ public final class CSVEncoder {
|
||||||
/// - Returns: A `CSVAsyncEncoder` instance with the current encoder's encoding
|
/// - Returns: A `CSVAsyncEncoder` instance with the current encoder's encoding
|
||||||
/// options and the `onRow` closure as its callback.
|
/// options and the `onRow` closure as its callback.
|
||||||
public func async(_ onRow: @escaping ([UInt8]) -> ()) -> CSVAsyncEncoder {
|
public func async(_ onRow: @escaping ([UInt8]) -> ()) -> CSVAsyncEncoder {
|
||||||
return CSVAsyncEncoder(encodingOptions: self.encodingOptions, onRow: onRow)
|
return CSVAsyncEncoder(encodingOptions: self.encodingOptions, configuration: self.configuration, onRow: onRow)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,9 +75,11 @@ public final class CSVEncoder {
|
||||||
/// You can get an instance of the `CSVSyncEncoder` with the `CSVEncoder.sync` property.
|
/// You can get an instance of the `CSVSyncEncoder` with the `CSVEncoder.sync` property.
|
||||||
public final class CSVSyncEncoder {
|
public final class CSVSyncEncoder {
|
||||||
internal var encodingOptions: CSVCodingOptions
|
internal var encodingOptions: CSVCodingOptions
|
||||||
|
internal var configuration: Config
|
||||||
|
|
||||||
internal init(encodingOptions: CSVCodingOptions) {
|
internal init(encodingOptions: CSVCodingOptions, configuration: Config = Config.default) {
|
||||||
self.encodingOptions = encodingOptions
|
self.encodingOptions = encodingOptions
|
||||||
|
self.configuration = configuration
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encodes an array of encodable objects into a single CSV document.
|
/// Encodes an array of encodable objects into a single CSV document.
|
||||||
|
@ -83,7 +92,7 @@ public final class CSVSyncEncoder {
|
||||||
var rows: [[UInt8]] = []
|
var rows: [[UInt8]] = []
|
||||||
rows.reserveCapacity(objects.count)
|
rows.reserveCapacity(objects.count)
|
||||||
|
|
||||||
let encoder = AsyncEncoder(encodingOptions: self.encodingOptions) { row in
|
let encoder = AsyncEncoder(encodingOptions: self.encodingOptions, configuration: self.configuration) { row in
|
||||||
rows.append(row)
|
rows.append(row)
|
||||||
}
|
}
|
||||||
try objects.forEach(encoder.encode)
|
try objects.forEach(encoder.encode)
|
||||||
|
@ -99,9 +108,9 @@ public final class CSVAsyncEncoder {
|
||||||
internal var encodingOptions: CSVCodingOptions
|
internal var encodingOptions: CSVCodingOptions
|
||||||
private var encoder: AsyncEncoder
|
private var encoder: AsyncEncoder
|
||||||
|
|
||||||
internal init(encodingOptions: CSVCodingOptions, onRow: @escaping ([UInt8]) -> ()) {
|
internal init(encodingOptions: CSVCodingOptions, configuration: Config = Config.default, onRow: @escaping ([UInt8]) -> ()) {
|
||||||
self.encodingOptions = encodingOptions
|
self.encodingOptions = encodingOptions
|
||||||
self.encoder = AsyncEncoder(encodingOptions: encodingOptions, onRow: onRow)
|
self.encoder = AsyncEncoder(encodingOptions: encodingOptions, configuration: configuration, onRow: onRow)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encodes an `Encodable` object into a row for a CSV document and passes it into
|
/// Encodes an `Encodable` object into a row for a CSV document and passes it into
|
||||||
|
@ -142,19 +151,31 @@ public final class CSVDecoder {
|
||||||
///
|
///
|
||||||
/// This is currently used to specify how `nil` and `Bool` values should be handled.
|
/// This is currently used to specify how `nil` and `Bool` values should be handled.
|
||||||
public var decodingOptions: CSVCodingOptions
|
public var decodingOptions: CSVCodingOptions
|
||||||
|
|
||||||
|
/// The CSV configuration to use when decoding or encoding
|
||||||
|
///
|
||||||
|
/// This is used to specify if cells are wrapped in quotes and what the delimiter is (comma or tab, etc.)
|
||||||
|
public var configuration: Config
|
||||||
|
|
||||||
/// Creates a new `CSVDecoder` instance.
|
/// Creates a new `CSVDecoder` instance.
|
||||||
///
|
///
|
||||||
/// - Parameter decodingOptions: The decoding options to use when decoding data to an object.
|
/// - Parameter decodingOptions: The decoding options to use when decoding data to an object.
|
||||||
public init(decodingOptions: CSVCodingOptions = .default) {
|
|
||||||
|
/// Creates a new `CSVDecoder` instance.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - decodingOptions: The decoding options to use when decoding data to an object.
|
||||||
|
/// - configuration: The struct that configures serialization options
|
||||||
|
public init(decodingOptions: CSVCodingOptions = .default, configuration: Config = Config.default) {
|
||||||
self.decodingOptions = decodingOptions
|
self.decodingOptions = decodingOptions
|
||||||
|
self.configuration = configuration
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a `CSVSyncDecoder` with the registered encoding options.
|
/// Creates a `CSVSyncDecoder` with the registered encoding options.
|
||||||
///
|
///
|
||||||
/// This decoder is for if you have whole CSV document you want to decode at once.
|
/// This decoder is for if you have whole CSV document you want to decode at once.
|
||||||
public var sync: CSVSyncDecoder {
|
public var sync: CSVSyncDecoder {
|
||||||
return CSVSyncDecoder(decodingOptions: self.decodingOptions)
|
return CSVSyncDecoder(decodingOptions: self.decodingOptions, configuration: self.configuration)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a `CSVAsyncDecoder` instance with the registered encoding options.
|
/// Creates a `CSVAsyncDecoder` instance with the registered encoding options.
|
||||||
|
@ -176,7 +197,8 @@ public final class CSVDecoder {
|
||||||
decoding: D.self,
|
decoding: D.self,
|
||||||
onInstance: onInstance,
|
onInstance: onInstance,
|
||||||
length: length,
|
length: length,
|
||||||
decodingOptions: self.decodingOptions
|
decodingOptions: self.decodingOptions,
|
||||||
|
configuration: self.configuration
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -186,9 +208,11 @@ public final class CSVDecoder {
|
||||||
/// You can get an instance of `CSVSyncDecoder` from the `CSVDecoder.sync` property.
|
/// You can get an instance of `CSVSyncDecoder` from the `CSVDecoder.sync` property.
|
||||||
public final class CSVSyncDecoder {
|
public final class CSVSyncDecoder {
|
||||||
internal var decodingOptions: CSVCodingOptions
|
internal var decodingOptions: CSVCodingOptions
|
||||||
|
internal var configuration: Config
|
||||||
|
|
||||||
internal init(decodingOptions: CSVCodingOptions) {
|
internal init(decodingOptions: CSVCodingOptions, configuration: Config = Config.default) {
|
||||||
self.decodingOptions = decodingOptions
|
self.decodingOptions = decodingOptions
|
||||||
|
self.configuration = configuration
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decodes a whole CSV document into an array of a specified `Decodable` type.
|
/// Decodes a whole CSV document into an array of a specified `Decodable` type.
|
||||||
|
@ -224,9 +248,10 @@ public final class CSVAsyncDecoder {
|
||||||
internal var length: Int
|
internal var length: Int
|
||||||
internal var decoding: Decodable.Type
|
internal var decoding: Decodable.Type
|
||||||
internal var decodingOptions: CSVCodingOptions
|
internal var decodingOptions: CSVCodingOptions
|
||||||
|
internal var configuration: Config
|
||||||
private var rowDecoder: AsyncDecoder
|
private var rowDecoder: AsyncDecoder
|
||||||
|
|
||||||
internal init<D>(decoding: D.Type, onInstance: @escaping (D) -> (), length: Int, decodingOptions: CSVCodingOptions)
|
internal init<D>(decoding: D.Type, onInstance: @escaping (D) -> (), length: Int, decodingOptions: CSVCodingOptions, configuration: Config = Config.default)
|
||||||
where D: Decodable
|
where D: Decodable
|
||||||
{
|
{
|
||||||
let callback = { (decoded: Decodable) in
|
let callback = { (decoded: Decodable) in
|
||||||
|
@ -240,10 +265,12 @@ public final class CSVAsyncDecoder {
|
||||||
self.length = length
|
self.length = length
|
||||||
self.decoding = decoding
|
self.decoding = decoding
|
||||||
self.decodingOptions = decodingOptions
|
self.decodingOptions = decodingOptions
|
||||||
|
self.configuration = configuration
|
||||||
self.rowDecoder = AsyncDecoder(
|
self.rowDecoder = AsyncDecoder(
|
||||||
decoding: D.self,
|
decoding: D.self,
|
||||||
path: [],
|
path: [],
|
||||||
decodingOptions: decodingOptions,
|
decodingOptions: decodingOptions,
|
||||||
|
configuration: configuration,
|
||||||
onInstance: callback
|
onInstance: callback
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -260,7 +287,7 @@ public final class CSVAsyncDecoder {
|
||||||
///
|
///
|
||||||
/// - Parameter data: A section of the CSV document to decode.
|
/// - Parameter data: A section of the CSV document to decode.
|
||||||
/// - Throws: Errors that occur during the decoding process.
|
/// - Throws: Errors that occur during the decoding process.
|
||||||
public func decode<C>(_ data: C)throws where C: Collection, C.Element == UInt8 {
|
public func decode<C>(_ data: C) throws where C: Collection, C.Element == UInt8 {
|
||||||
try self.rowDecoder.decode(Array(data), length: self.length)
|
try self.rowDecoder.decode(Array(data), length: self.length)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,12 +20,13 @@ internal final class AsyncDecoder: Decoder {
|
||||||
info: [CodingUserInfoKey : Any] = [:],
|
info: [CodingUserInfoKey : Any] = [:],
|
||||||
data: Storage = .none,
|
data: Storage = .none,
|
||||||
decodingOptions: CSVCodingOptions,
|
decodingOptions: CSVCodingOptions,
|
||||||
|
configuration: Config = Config.default,
|
||||||
onInstance: @escaping (Decodable)throws -> ()
|
onInstance: @escaping (Decodable)throws -> ()
|
||||||
) {
|
) {
|
||||||
self.codingPath = path
|
self.codingPath = path
|
||||||
self.userInfo = info
|
self.userInfo = info
|
||||||
self.decoding = decoding
|
self.decoding = decoding
|
||||||
self.handler = AsyncDecoderHandler { _ in return }
|
self.handler = AsyncDecoderHandler(configuration: configuration){ _ in return }
|
||||||
self.decodingOptions = decodingOptions
|
self.decodingOptions = decodingOptions
|
||||||
self.onInstance = onInstance
|
self.onInstance = onInstance
|
||||||
self.data = data
|
self.data = data
|
||||||
|
@ -82,8 +83,8 @@ internal final class AsyncDecoderHandler {
|
||||||
private var columnCount: Int
|
private var columnCount: Int
|
||||||
private var currentColumn: Int
|
private var currentColumn: Int
|
||||||
|
|
||||||
init(onRow: @escaping ([String: [UInt8]])throws -> ()) {
|
init(configuration: Config = Config.default, onRow: @escaping ([String: [UInt8]])throws -> ()) {
|
||||||
self.parser = Parser()
|
self.parser = Parser(configuration: configuration)
|
||||||
self.currentRow = [:]
|
self.currentRow = [:]
|
||||||
self.onRow = onRow
|
self.onRow = onRow
|
||||||
self.columnCount = 0
|
self.columnCount = 0
|
||||||
|
|
|
@ -5,18 +5,21 @@ final class AsyncEncoder: Encoder {
|
||||||
let userInfo: [CodingUserInfoKey : Any]
|
let userInfo: [CodingUserInfoKey : Any]
|
||||||
let container: DataContainer
|
let container: DataContainer
|
||||||
let encodingOptions: CSVCodingOptions
|
let encodingOptions: CSVCodingOptions
|
||||||
|
let configuration: Config
|
||||||
let onRow: ([UInt8]) -> ()
|
let onRow: ([UInt8]) -> ()
|
||||||
|
|
||||||
init(
|
init(
|
||||||
path: [CodingKey] = [],
|
path: [CodingKey] = [],
|
||||||
info: [CodingUserInfoKey : Any] = [:],
|
info: [CodingUserInfoKey : Any] = [:],
|
||||||
encodingOptions: CSVCodingOptions,
|
encodingOptions: CSVCodingOptions,
|
||||||
|
configuration: Config = Config.default,
|
||||||
onRow: @escaping ([UInt8]) -> ()
|
onRow: @escaping ([UInt8]) -> ()
|
||||||
) {
|
) {
|
||||||
self.codingPath = path
|
self.codingPath = path
|
||||||
self.userInfo = info
|
self.userInfo = info
|
||||||
self.container = DataContainer(section: .header)
|
self.container = DataContainer(section: .header)
|
||||||
self.encodingOptions = encodingOptions
|
self.encodingOptions = encodingOptions
|
||||||
|
self.configuration = configuration
|
||||||
self.onRow = onRow
|
self.onRow = onRow
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,14 +42,14 @@ final class AsyncEncoder: Encoder {
|
||||||
switch self.container.section {
|
switch self.container.section {
|
||||||
case .header:
|
case .header:
|
||||||
try object.encode(to: self)
|
try object.encode(to: self)
|
||||||
self.onRow(Array(self.container.cells.joined(separator: [44])))
|
self.onRow(Array(self.container.cells.joined(separator: [self.configuration.cellSeparator])))
|
||||||
self.container.section = .row
|
self.container.section = .row
|
||||||
self.container.rowCount += 1
|
self.container.rowCount += 1
|
||||||
self.container.cells = []
|
self.container.cells = []
|
||||||
fallthrough
|
fallthrough
|
||||||
case .row:
|
case .row:
|
||||||
try object.encode(to: self)
|
try object.encode(to: self)
|
||||||
self.onRow(Array(self.container.cells.joined(separator: [44])))
|
self.onRow(Array(self.container.cells.joined(separator: [self.configuration.cellSeparator])))
|
||||||
self.container.rowCount += 1
|
self.container.rowCount += 1
|
||||||
self.container.cells = []
|
self.container.cells = []
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,14 +8,18 @@ final class AsyncKeyedEncoder<K>: KeyedEncodingContainerProtocol where K: Coding
|
||||||
self.codingPath = path
|
self.codingPath = path
|
||||||
self.encoder = encoder
|
self.encoder = encoder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var delimiter: UInt8? {
|
||||||
|
return self.encoder.configuration.cellDelimiter
|
||||||
|
}
|
||||||
|
|
||||||
func _encode(_ value: [UInt8], for key: K) {
|
func _encode(_ value: [UInt8], for key: K) {
|
||||||
switch self.encoder.container.section {
|
switch self.encoder.container.section {
|
||||||
case .header:
|
case .header:
|
||||||
let bytes = Array([[34], key.stringValue.bytes.escaped, [34]].joined())
|
let bytes = key.stringValue.bytes.escaping(self.delimiter)
|
||||||
self.encoder.container.cells.append(bytes)
|
self.encoder.container.cells.append(bytes)
|
||||||
case .row:
|
case .row:
|
||||||
let bytes = Array([[34], value.escaped, [34]].joined())
|
let bytes = value.escaping(self.delimiter)
|
||||||
self.encoder.container.cells.append(bytes)
|
self.encoder.container.cells.append(bytes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,9 +45,17 @@ final class AsyncKeyedEncoder<K>: KeyedEncodingContainerProtocol where K: Coding
|
||||||
func encode(_ value: String, forKey key: K) throws { self._encode(value.bytes, for: key) }
|
func encode(_ value: String, forKey key: K) throws { self._encode(value.bytes, for: key) }
|
||||||
|
|
||||||
func encode<T>(_ value: T, forKey key: K) throws where T : Encodable {
|
func encode<T>(_ value: T, forKey key: K) throws where T : Encodable {
|
||||||
let encoder = AsyncEncoder(encodingOptions: self.encoder.encodingOptions, onRow: self.encoder.onRow)
|
switch self.encoder.container.section {
|
||||||
try value.encode(to: encoder)
|
case .header: self.encoder.container.cells.append(key.stringValue.bytes.escaping(self.delimiter))
|
||||||
self._encode(encoder.container.cells[0], for: key)
|
case .row:
|
||||||
|
let encoder = AsyncEncoder(
|
||||||
|
encodingOptions: self.encoder.encodingOptions,
|
||||||
|
configuration: self.encoder.configuration,
|
||||||
|
onRow: self.encoder.onRow
|
||||||
|
)
|
||||||
|
try value.encode(to: encoder)
|
||||||
|
self.encoder.container.cells.append(encoder.container.cells[0])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func encodeIfPresent(_ value: Bool?, forKey key: K) throws {
|
func encodeIfPresent(_ value: Bool?, forKey key: K) throws {
|
||||||
|
|
|
@ -9,18 +9,22 @@ final class AsyncSingleValueEncoder: SingleValueEncodingContainer {
|
||||||
self.encoder = encoder
|
self.encoder = encoder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var delimiter: UInt8? {
|
||||||
|
return self.encoder.configuration.cellDelimiter
|
||||||
|
}
|
||||||
|
|
||||||
func encodeNil() throws {
|
func encodeNil() throws {
|
||||||
let value = self.encoder.encodingOptions.nilCodingStrategy.bytes()
|
let value = self.encoder.encodingOptions.nilCodingStrategy.bytes().escaping(self.delimiter)
|
||||||
self.encoder.container.cells.append(value.escaped)
|
self.encoder.container.cells.append(value)
|
||||||
}
|
}
|
||||||
func encode(_ value: Bool) throws {
|
func encode(_ value: Bool) throws {
|
||||||
let value = self.encoder.encodingOptions.boolCodingStrategy.bytes(from: value)
|
let value = self.encoder.encodingOptions.boolCodingStrategy.bytes(from: value).escaping(self.delimiter)
|
||||||
self.encoder.container.cells.append(value.escaped)
|
self.encoder.container.cells.append(value)
|
||||||
}
|
}
|
||||||
func encode(_ value: String) throws { self.encoder.container.cells.append(value.bytes.escaped) }
|
func encode(_ value: String) throws { self.encoder.container.cells.append(value.bytes.escaping(self.delimiter)) }
|
||||||
func encode(_ value: Double) throws { self.encoder.container.cells.append(value.bytes.escaped) }
|
func encode(_ value: Double) throws { self.encoder.container.cells.append(value.bytes.escaping(self.delimiter)) }
|
||||||
func encode(_ value: Float) throws { self.encoder.container.cells.append(value.bytes.escaped) }
|
func encode(_ value: Float) throws { self.encoder.container.cells.append(value.bytes.escaping(self.delimiter)) }
|
||||||
func encode(_ value: Int) throws { self.encoder.container.cells.append(value.bytes.escaped) }
|
func encode(_ value: Int) throws { self.encoder.container.cells.append(value.bytes.escaping(self.delimiter)) }
|
||||||
|
|
||||||
func encode<T>(_ value: T) throws where T : Encodable {
|
func encode<T>(_ value: T) throws where T : Encodable {
|
||||||
let column = self.codingPath.map { $0.stringValue }.joined(separator: ".")
|
let column = self.codingPath.map { $0.stringValue }.joined(separator: ".")
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
// Wraps the Configuration options for Parse/Encode/Decode
|
||||||
|
|
||||||
|
|
||||||
|
/// The `Config` struct allows for configuring `Parser` and `Serializer`
|
||||||
|
/// to allow for separators other than comma or string delimiters
|
||||||
|
/// like quotation marks
|
||||||
|
public struct Config {
|
||||||
|
|
||||||
|
/// The character that separates one cell from another.
|
||||||
|
public let cellSeparator: UInt8
|
||||||
|
|
||||||
|
/// The character that is used to denote the start and end of a cell's contents.
|
||||||
|
public let cellDelimiter: UInt8?
|
||||||
|
|
||||||
|
/// The deault `Config` instance that uses commas for cell separators and double quotes
|
||||||
|
/// for cell delimiters.
|
||||||
|
public static let `default`: Config = Config(cellSeparator: 44, cellDelimiter: 34)
|
||||||
|
|
||||||
|
/// Creates a new `Config` instance
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - cellSeparator: The character that separates one cell from another.
|
||||||
|
/// - cellDelimiter: The character that is used to denote the start and end of a cell's contents.
|
||||||
|
public init(cellSeparator: UInt8, cellDelimiter: UInt8?) {
|
||||||
|
self.cellSeparator = cellSeparator
|
||||||
|
self.cellDelimiter = cellDelimiter
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new `Config` instance from `UnicdeScalar` literals.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - separator: The `UnicodeScalar` for the separator between cells (`','`).
|
||||||
|
/// - delimiter: The `UnicdeScalar` for the delimiter that marks the start and end of a cell (`'"'`).
|
||||||
|
public init(separator: UnicodeScalar, delimiter: UnicodeScalar) {
|
||||||
|
self.cellSeparator = UInt8(ascii: separator)
|
||||||
|
self.cellDelimiter = UInt8(ascii: delimiter)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
// '\t' => 9
|
||||||
// '\n' => 10
|
// '\n' => 10
|
||||||
// '\r' => 13
|
// '\r' => 13
|
||||||
// '"' => 34
|
// '"' => 34
|
||||||
|
@ -69,6 +70,9 @@ public struct Parser {
|
||||||
|
|
||||||
/// The callback that is called when a cell is parsed.
|
/// The callback that is called when a cell is parsed.
|
||||||
public var onCell: CellHandler?
|
public var onCell: CellHandler?
|
||||||
|
|
||||||
|
/// The struct that configures parsing options
|
||||||
|
public var configuration: Config
|
||||||
|
|
||||||
private var state: State
|
private var state: State
|
||||||
|
|
||||||
|
@ -81,9 +85,11 @@ public struct Parser {
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - onHeader: The callback that will be called when a header is parsed.
|
/// - onHeader: The callback that will be called when a header is parsed.
|
||||||
/// - onCell: The callback that will be called when a cell is parsed.
|
/// - onCell: The callback that will be called when a cell is parsed.
|
||||||
public init(onHeader: HeaderHandler? = nil, onCell: CellHandler? = nil) {
|
/// - configuration: The struct that configures parsing options
|
||||||
|
public init(onHeader: HeaderHandler? = nil, onCell: CellHandler? = nil, configuration: Config = Config.default) {
|
||||||
self.onHeader = onHeader
|
self.onHeader = onHeader
|
||||||
self.onCell = onCell
|
self.onCell = onCell
|
||||||
|
self.configuration = configuration
|
||||||
|
|
||||||
self.state = State()
|
self.state = State()
|
||||||
}
|
}
|
||||||
|
@ -113,10 +119,10 @@ public struct Parser {
|
||||||
while index < data.endIndex {
|
while index < data.endIndex {
|
||||||
let byte = data[index]
|
let byte = data[index]
|
||||||
switch byte {
|
switch byte {
|
||||||
case 34:
|
case configuration.cellDelimiter:
|
||||||
currentCell.append(contentsOf: data[slice.start..<slice.end])
|
currentCell.append(contentsOf: data[slice.start..<slice.end])
|
||||||
slice = (index + 1, index + 1)
|
slice = (index + 1, index + 1)
|
||||||
switch self.state.inQuotes && index + 1 < data.endIndex && data[index + 1] == 34 {
|
switch self.state.inQuotes && index + 1 < data.endIndex && data[index + 1] == configuration.cellDelimiter {
|
||||||
case true: index += 1
|
case true: index += 1
|
||||||
case false: self.state.inQuotes.toggle()
|
case false: self.state.inQuotes.toggle()
|
||||||
}
|
}
|
||||||
|
@ -136,7 +142,7 @@ public struct Parser {
|
||||||
if self.state.position == .headers { updateState = true }
|
if self.state.position == .headers { updateState = true }
|
||||||
fallthrough
|
fallthrough
|
||||||
}
|
}
|
||||||
case 44:
|
case configuration.cellSeparator:
|
||||||
if self.state.inQuotes {
|
if self.state.inQuotes {
|
||||||
slice.end += 1
|
slice.end += 1
|
||||||
} else {
|
} else {
|
||||||
|
@ -190,9 +196,15 @@ public struct Parser {
|
||||||
|
|
||||||
/// A synchronous wrapper for the `Parser` type for parsing whole CSV documents at once.
|
/// A synchronous wrapper for the `Parser` type for parsing whole CSV documents at once.
|
||||||
public final class SyncParser {
|
public final class SyncParser {
|
||||||
|
|
||||||
|
/// The struct configures parsing options
|
||||||
|
public var configuration: Config
|
||||||
|
|
||||||
/// Creates a new `SyncParser` instance
|
/// Creates a new `SyncParser` instance
|
||||||
public init() {}
|
///
|
||||||
|
/// - Parameter configuration: The struct configures parsing options
|
||||||
|
|
||||||
|
public init(configuration: Config = Config.default ) { self.configuration = configuration }
|
||||||
|
|
||||||
/// Parses a whole CSV document at once.
|
/// Parses a whole CSV document at once.
|
||||||
///
|
///
|
||||||
|
@ -207,7 +219,8 @@ public final class SyncParser {
|
||||||
},
|
},
|
||||||
onCell: { header, cell in
|
onCell: { header, cell in
|
||||||
results[header, default: []].append(cell.count > 0 ? cell : nil)
|
results[header, default: []].append(cell.count > 0 ? cell : nil)
|
||||||
}
|
},
|
||||||
|
configuration: configuration
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.parse(data)
|
parser.parse(data)
|
||||||
|
@ -229,7 +242,8 @@ public final class SyncParser {
|
||||||
let title = String(decoding: header, as: UTF8.self)
|
let title = String(decoding: header, as: UTF8.self)
|
||||||
let contents = String(decoding: cell, as: UTF8.self)
|
let contents = String(decoding: cell, as: UTF8.self)
|
||||||
results[title, default: []].append(cell.count > 0 ? contents : nil)
|
results[title, default: []].append(cell.count > 0 ? contents : nil)
|
||||||
}
|
},
|
||||||
|
configuration: configuration
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.parse(Array(data.utf8))
|
parser.parse(Array(data.utf8))
|
||||||
|
|
|
@ -61,15 +61,21 @@ extension Dictionary: KeyedCollection { }
|
||||||
/// - Note: You should create a new `Serializer` dictionary you serialize.
|
/// - Note: You should create a new `Serializer` dictionary you serialize.
|
||||||
public struct Serializer {
|
public struct Serializer {
|
||||||
private var serializedHeaders: Bool
|
private var serializedHeaders: Bool
|
||||||
|
|
||||||
|
/// The struct configures serialization options
|
||||||
|
var configuration: Config
|
||||||
|
|
||||||
/// The callback that will be called with each row that is serialized.
|
/// The callback that will be called with each row that is serialized.
|
||||||
public var onRow: ([UInt8])throws -> ()
|
public var onRow: ([UInt8])throws -> ()
|
||||||
|
|
||||||
/// Creates a new `Serializer` instance.
|
/// Creates a new `Serializer` instance.
|
||||||
///
|
///
|
||||||
/// - Parameter onRow: The callback that will be called with each row that is serialized.
|
/// - Parameter: The struct configures serialization options
|
||||||
public init(onRow: @escaping ([UInt8])throws -> ()) {
|
/// - configuration: The struct that configures serialization options
|
||||||
|
/// - onRow: The callback that will be called with each row that is serialized.
|
||||||
|
public init(configuration: Config = Config.default, onRow: @escaping ([UInt8])throws -> ()) {
|
||||||
self.serializedHeaders = false
|
self.serializedHeaders = false
|
||||||
|
self.configuration = configuration
|
||||||
self.onRow = onRow
|
self.onRow = onRow
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,8 +100,10 @@ public struct Serializer {
|
||||||
guard data.count > 0 else { return errors.result }
|
guard data.count > 0 else { return errors.result }
|
||||||
|
|
||||||
if !self.serializedHeaders {
|
if !self.serializedHeaders {
|
||||||
let headers = data.keys.map { title in Array([[34], title.bytes.escaped, [34]].joined()) }
|
let headers = data.keys.map { title -> [UInt8] in
|
||||||
do { try self.onRow(Array(headers.joined(separator: [44]))) }
|
return title.bytes.escaping(self.configuration.cellDelimiter)
|
||||||
|
}
|
||||||
|
do { try self.onRow(Array(headers.joined(separator: [configuration.cellSeparator]))) }
|
||||||
catch let error { errors.errors.append(error) }
|
catch let error { errors.errors.append(error) }
|
||||||
self.serializedHeaders = true
|
self.serializedHeaders = true
|
||||||
}
|
}
|
||||||
|
@ -103,9 +111,9 @@ public struct Serializer {
|
||||||
guard let first = data.first?.value else { return errors.result }
|
guard let first = data.first?.value else { return errors.result }
|
||||||
(first.startIndex..<first.endIndex).forEach { index in
|
(first.startIndex..<first.endIndex).forEach { index in
|
||||||
let cells = data.values.map { column -> [UInt8] in
|
let cells = data.values.map { column -> [UInt8] in
|
||||||
return Array([[34], column[index].bytes.escaped, [34]].joined())
|
return column[index].bytes.escaping(self.configuration.cellDelimiter)
|
||||||
}
|
}
|
||||||
do { try onRow(Array(cells.joined(separator: [44]))) }
|
do { try onRow(Array(cells.joined(separator: [configuration.cellSeparator]))) }
|
||||||
catch let error { errors.errors.append(error) }
|
catch let error { errors.errors.append(error) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,8 +124,15 @@ public struct Serializer {
|
||||||
/// A synchronous wrapper for the `Serializer` struct for parsing a whole CSV document.
|
/// A synchronous wrapper for the `Serializer` struct for parsing a whole CSV document.
|
||||||
public struct SyncSerializer {
|
public struct SyncSerializer {
|
||||||
|
|
||||||
|
/// The serilization options for the `SyncSerializer` instance.
|
||||||
|
let configuration: Config
|
||||||
|
|
||||||
/// Creates a new `SyncSerializer` instance.
|
/// Creates a new `SyncSerializer` instance.
|
||||||
public init () { }
|
///
|
||||||
|
/// - Parameter configuration: The serilization options for the `SyncSerializer` instance.
|
||||||
|
public init (configuration: Config = Config.default) {
|
||||||
|
self.configuration = configuration
|
||||||
|
}
|
||||||
|
|
||||||
/// Serializes a dictionary to CSV document data. Usually this will be a dictionary of type
|
/// Serializes a dictionary to CSV document data. Usually this will be a dictionary of type
|
||||||
/// `[BytesRepresentable: [BytesRepresentable]], but it can be any type you conform to the proper protocols.
|
/// `[BytesRepresentable: [BytesRepresentable]], but it can be any type you conform to the proper protocols.
|
||||||
|
@ -134,7 +149,7 @@ public struct SyncSerializer {
|
||||||
var rows: [[UInt8]] = []
|
var rows: [[UInt8]] = []
|
||||||
rows.reserveCapacity(data.first?.value.count ?? 0)
|
rows.reserveCapacity(data.first?.value.count ?? 0)
|
||||||
|
|
||||||
var serializer = Serializer { row in rows.append(row) }
|
var serializer = Serializer(configuration: self.configuration) { row in rows.append(row) }
|
||||||
serializer.serialize(data)
|
serializer.serialize(data)
|
||||||
|
|
||||||
return Array(rows.joined(separator: [10]))
|
return Array(rows.joined(separator: [10]))
|
||||||
|
|
|
@ -12,8 +12,15 @@ extension String {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Array where Element == UInt8 {
|
extension Array where Element == UInt8 {
|
||||||
var escaped: [UInt8] {
|
func escaping(_ character: UInt8?) -> [UInt8] {
|
||||||
return self.contains(34) ? Array(self.split(separator: 34).joined(separator: [34, 34])) : self
|
guard let code = character else {
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
let contents = self.contains(code) ?
|
||||||
|
Array(self.split(separator: code, omittingEmptySubsequences: false).joined(separator: [code, code])) :
|
||||||
|
self
|
||||||
|
return Array([[code], contents, [code]].joined())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -44,7 +44,7 @@ final class DecoderTests: XCTestCase {
|
||||||
for _ in 0..<1_000 {
|
for _ in 0..<1_000 {
|
||||||
let decoder = CSVDecoder().async(for: Person.self, length: contentLength) { _ in return }
|
let decoder = CSVDecoder().async(for: Person.self, length: contentLength) { _ in return }
|
||||||
do {
|
do {
|
||||||
try bytes.forEach(decoder.decode)
|
try bytes.forEach{ try decoder.decode($0) }
|
||||||
} catch let error as DecodingError {
|
} catch let error as DecodingError {
|
||||||
XCTFail(error.failureReason ?? "No failure reason")
|
XCTFail(error.failureReason ?? "No failure reason")
|
||||||
error.errorDescription.map { print($0) }
|
error.errorDescription.map { print($0) }
|
||||||
|
|
|
@ -59,6 +59,26 @@ final class EncoderTests: XCTestCase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testEscapingDelimiters() throws {
|
||||||
|
let quotePerson = Person(firstName: "A", lastName: "J", age: 42, gender: .male, tagLine: #"All "with quotes""#)
|
||||||
|
let hashPerson = Person(firstName: "M", lastName: "A", age: 28, gender: .female, tagLine: "#iWin#")
|
||||||
|
|
||||||
|
let quoteResult = """
|
||||||
|
"first name","last_name","age","gender","tagLine"
|
||||||
|
"A","J","42","M","All ""with quotes""\"
|
||||||
|
"""
|
||||||
|
let hashResult = """
|
||||||
|
#first name#,#last_name#,#age#,#gender#,#tagLine#
|
||||||
|
#M#,#A#,#28#,#F#,###iWin###
|
||||||
|
"""
|
||||||
|
|
||||||
|
let quoteEncoder = CSVEncoder().sync
|
||||||
|
let hashEncoder = CSVEncoder(encodingOptions: .default, configuration: .init(cellSeparator: 44, cellDelimiter: 35)).sync
|
||||||
|
|
||||||
|
try XCTAssertEqual(quoteEncoder.encode([quotePerson]), Data(quoteResult.utf8))
|
||||||
|
try XCTAssertEqual(hashEncoder.encode([hashPerson]), Data(hashResult.utf8))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate struct Person: Codable, Equatable {
|
fileprivate struct Person: Codable, Equatable {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import CSV
|
||||||
|
|
||||||
final class SerializerTests: XCTestCase {
|
final class SerializerTests: XCTestCase {
|
||||||
func testSyncSerialize() {
|
func testSyncSerialize() {
|
||||||
let serializer = SyncSerializer()
|
let serializer = SyncSerializer(configuration: Config.default)
|
||||||
let serialized = serializer.serialize(orderedData)
|
let serialized = serializer.serialize(orderedData)
|
||||||
let string = String(decoding: serialized, as: UTF8.self)
|
let string = String(decoding: serialized, as: UTF8.self)
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ final class SerializerTests: XCTestCase {
|
||||||
|
|
||||||
func testChunkedSerialize() throws {
|
func testChunkedSerialize() throws {
|
||||||
var rows: [[UInt8]] = []
|
var rows: [[UInt8]] = []
|
||||||
var serializer = Serializer { row in rows.append(row) }
|
var serializer = Serializer(configuration: Config.default) { row in rows.append(row) }
|
||||||
for chunk in orderedChunks {
|
for chunk in orderedChunks {
|
||||||
try serializer.serialize(chunk).get()
|
try serializer.serialize(chunk).get()
|
||||||
}
|
}
|
||||||
|
@ -42,6 +42,28 @@ final class SerializerTests: XCTestCase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testEscapedDelimiter() {
|
||||||
|
let quoteData: OrderedKeyedCollection = ["list": ["Standard string", #"A string with "quotes""#]]
|
||||||
|
let hashData: OrderedKeyedCollection = ["list": ["Some string without hashes", "A #string with# hashes"]]
|
||||||
|
|
||||||
|
let quoteResult = """
|
||||||
|
"list"
|
||||||
|
"Standard string"
|
||||||
|
"A string with ""quotes""\"
|
||||||
|
"""
|
||||||
|
let hashResult = """
|
||||||
|
#list#
|
||||||
|
#Some string without hashes#
|
||||||
|
#A ##string with## hashes#
|
||||||
|
"""
|
||||||
|
|
||||||
|
let quoteSerializer = SyncSerializer()
|
||||||
|
let hashSerializer = SyncSerializer(configuration: .init(cellSeparator: 44, cellDelimiter: 35))
|
||||||
|
|
||||||
|
XCTAssertEqual(quoteSerializer.serialize(quoteData), Array(quoteResult.utf8))
|
||||||
|
XCTAssertEqual(hashSerializer.serialize(hashData), Array(hashResult.utf8))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal struct OrderedKeyedCollection<K, V>: KeyedCollection, ExpressibleByDictionaryLiteral where K: Hashable {
|
internal struct OrderedKeyedCollection<K, V>: KeyedCollection, ExpressibleByDictionaryLiteral where K: Hashable {
|
||||||
|
|
|
@ -16,7 +16,7 @@ final class StressTests: XCTestCase {
|
||||||
var parser = Parser(onHeader: { _ in return }, onCell: { _, _ in return })
|
var parser = Parser(onHeader: { _ in return }, onCell: { _, _ in return })
|
||||||
let csv = Array(data)
|
let csv = Array(data)
|
||||||
|
|
||||||
// Baseline: 4.630
|
// Baseline: 3.686
|
||||||
measure {
|
measure {
|
||||||
parser.parse(csv)
|
parser.parse(csv)
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ final class StressTests: XCTestCase {
|
||||||
let parser = SyncParser()
|
let parser = SyncParser()
|
||||||
let csv = Array(data)
|
let csv = Array(data)
|
||||||
|
|
||||||
// Baseline: 10.825
|
// Baseline: 10.510
|
||||||
// Time to beat: 9.142
|
// Time to beat: 9.142
|
||||||
measure {
|
measure {
|
||||||
_ = parser.parse(csv)
|
_ = parser.parse(csv)
|
||||||
|
@ -38,7 +38,7 @@ final class StressTests: XCTestCase {
|
||||||
let csv = Array(data)
|
let csv = Array(data)
|
||||||
let parsed = SyncParser().parse(csv)
|
let parsed = SyncParser().parse(csv)
|
||||||
|
|
||||||
// Baseline: 18.957
|
// Baseline: 21.453
|
||||||
// Time to beat: 11.932
|
// Time to beat: 11.932
|
||||||
measure {
|
measure {
|
||||||
serializer.serialize(parsed)
|
serializer.serialize(parsed)
|
||||||
|
@ -50,7 +50,7 @@ final class StressTests: XCTestCase {
|
||||||
let csv = Array(data)
|
let csv = Array(data)
|
||||||
let parsed = SyncParser().parse(csv)
|
let parsed = SyncParser().parse(csv)
|
||||||
|
|
||||||
// Baseline: 18.047
|
// Baseline: 22.903
|
||||||
// Time to beat: 11.932
|
// Time to beat: 11.932
|
||||||
measure {
|
measure {
|
||||||
_ = serializer.serialize(parsed)
|
_ = serializer.serialize(parsed)
|
||||||
|
@ -60,7 +60,7 @@ final class StressTests: XCTestCase {
|
||||||
func testMeasureAsyncDecoding() {
|
func testMeasureAsyncDecoding() {
|
||||||
let decoder = CSVDecoder(decodingOptions: self.codingOptions)
|
let decoder = CSVDecoder(decodingOptions: self.codingOptions)
|
||||||
|
|
||||||
// Baseline: 14.347
|
// Baseline: 18.217
|
||||||
measure {
|
measure {
|
||||||
do {
|
do {
|
||||||
let async = decoder.async(
|
let async = decoder.async(
|
||||||
|
@ -82,7 +82,7 @@ final class StressTests: XCTestCase {
|
||||||
func testMeasureSyncDecoding() {
|
func testMeasureSyncDecoding() {
|
||||||
let decoder = CSVDecoder(decodingOptions: self.codingOptions).sync
|
let decoder = CSVDecoder(decodingOptions: self.codingOptions).sync
|
||||||
|
|
||||||
// Baseline: 19.736
|
// Baseline: 18.970
|
||||||
// Time to beat: 18.489
|
// Time to beat: 18.489
|
||||||
measure {
|
measure {
|
||||||
do {
|
do {
|
||||||
|
@ -97,7 +97,7 @@ final class StressTests: XCTestCase {
|
||||||
let people = try CSVDecoder(decodingOptions: self.codingOptions).sync.decode(Response.self, from: data)
|
let people = try CSVDecoder(decodingOptions: self.codingOptions).sync.decode(Response.self, from: data)
|
||||||
let encoder = CSVEncoder(encodingOptions: codingOptions)
|
let encoder = CSVEncoder(encodingOptions: codingOptions)
|
||||||
|
|
||||||
// Baseline: 11.477
|
// Baseline: 14.969
|
||||||
// Time to beat: 9.477
|
// Time to beat: 9.477
|
||||||
measure {
|
measure {
|
||||||
do {
|
do {
|
||||||
|
@ -113,7 +113,7 @@ final class StressTests: XCTestCase {
|
||||||
let people = try CSVDecoder(decodingOptions: self.codingOptions).sync.decode(Response.self, from: data)
|
let people = try CSVDecoder(decodingOptions: self.codingOptions).sync.decode(Response.self, from: data)
|
||||||
let encoder = CSVEncoder(encodingOptions: codingOptions).sync
|
let encoder = CSVEncoder(encodingOptions: codingOptions).sync
|
||||||
|
|
||||||
// Baseline: 13.412
|
// Baseline: 18.147
|
||||||
// Time to beat: 9.477
|
// Time to beat: 9.477
|
||||||
measure {
|
measure {
|
||||||
do {
|
do {
|
||||||
|
|
Loading…
Reference in New Issue