From 5cfe55b84df2f60001e9b73b3a118e32f998f1d9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marcos=20S=C3=A1nchez-Dehesa=20Carballo?=
Date: Thu, 7 May 2020 00:45:45 +0200
Subject: [PATCH] LazyEncoder added
---
README.md | 67 ++++-
sources/Delimiter.swift | 2 +-
sources/Deprecated.swift | 17 ++
sources/declarative/decodable/Decoder.swift | 18 +-
.../declarative/decodable/DecoderLazy.swift | 52 +++-
sources/declarative/encodable/Encoder.swift | 38 ++-
.../declarative/encodable/EncoderLazy.swift | 81 +++++
.../declarative/encodable/internal/Sink.swift | 15 +-
.../encodable/internal/SinkBuffer.swift | 2 +-
sources/imperative/writer/Writer.swift | 13 +-
sources/imperative/writer/WriterAPI.swift | 8 +-
tests/ActiveTests/WriterTests.swift | 16 +-
tests/CodableTests/EncodingLazyTests.swift | 282 ++++++++++++++++++
.../EncodingRegularUsageTests.swift | 10 +-
14 files changed, 561 insertions(+), 60 deletions(-)
create mode 100644 sources/declarative/encodable/EncoderLazy.swift
create mode 100644 tests/CodableTests/EncodingLazyTests.swift
diff --git a/README.md b/README.md
index 120ec7b..a0b2b96 100644
--- a/README.md
+++ b/README.md
@@ -213,7 +213,7 @@ A `CSVWriter` encodes CSV information into a specified target (i.e. a `String`,
for row in input {
try writer.write(row: row)
}
- try writer.endFile()
+ try writer.endEncoding()
```
Alternatively, you may write directly to a buffer in memory and access its `Data` representation.
@@ -223,7 +223,7 @@ A `CSVWriter` encodes CSV information into a specified target (i.e. a `String`,
for row in input.dropFirst() {
try writer.write(row: row)
}
- try writer.endFile()
+ try writer.endEncoding()
let result = try writer.data()
```
@@ -241,7 +241,7 @@ A `CSVWriter` encodes CSV information into a specified target (i.e. a `String`,
try writer.write(fields: input[2])
try writer.endRow()
- try writer.endFile()
+ try writer.endEncoding()
```
`CSVWriter` has a wealth of low-level imperative APIs, that let you write one field, several fields at a time, end a row, write an empty row, etc.
@@ -330,7 +330,7 @@ let decoder = CSVDecoder()
let result = try decoder.decode(CustomType.self, from: data)
```
-`CSVDecoder` can decode CSVs represented as a `Data` blob, a `String`, or an actual file in the file system.
+`CSVDecoder` can decode CSVs represented as a `Data` blob, a `String`, an actual file in the file system, or an `InputStream` (e.g. `stdin`).
```swift
let decoder = CSVDecoder { $0.bufferingStrategy = .sequential }
@@ -377,33 +377,33 @@ decoder.decimalStrategy = .custom { (decoder) in
-CSVDecoder.LazySequence
.
+CSVDecoder.LazyDecoder
.
A CSV input can be decoded _on demand_ with the decoder's `lazy(from:)` function.
```swift
-var sequence = CSVDecoder().lazy(from: fileURL)
+let lazyDecoder = CSVDecoder().lazy(from: fileURL)
while let row = sequence.next() {
let student = try row.decode(Student.self)
// Do something here
}
```
-`LazySequence` conforms to Swift's [`Sequence` protocol](https://developer.apple.com/documentation/swift/sequence), letting you use functionality such as `map()`, `allSatisfy()`, etc. Please note, `LazySequence` cannot be used for repeated access. It _consumes_ the input CSV.
+`LazyDecoder` conforms to Swift's [`Sequence` protocol](https://developer.apple.com/documentation/swift/sequence), letting you use functionality such as `map()`, `allSatisfy()`, etc. Please note, `LazyDecoder` cannot be used for repeated access. It _consumes_ the input CSV.
```swift
-var sequence = decoder.lazy(from: fileData)
-let students = try sequence.map { try $0.decode(Student.self) }
+let lazyDecoder = CSVDecoder(configuration: config).lazy(from: fileData)
+let students = try lazyDecoder.map { try $0.decode(Student.self) }
```
A nice benefit of using the _lazy_ operation, is that it lets you switch how a row is decoded at any point. For example:
```swift
-var sequence = decoder.lazy(from: fileString)
-let students = zip( 0..<100, sequence) { (_, row) in row.decode(Student.self) }
-let teachers = zip(100..<110, sequence) { (_, row) in row.decode(Teacher.self) }
+let lazyDecoder = decoder.lazy(from: fileString)
+let students = ( 0..<100).map { _ in try lazyDecoder.decode(Student.self) }
+let teachers = (100..<110).map { _ in try lazyDecoder.decode(Teacher.self) }
```
-Since `LazySequence` exclusively provides sequential access; setting the buffering strategy to `.sequential` will reduce the decoder's memory usage.
+Since `LazyDecoder` exclusively provides sequential access; setting the buffering strategy to `.sequential` will reduce the decoder's memory usage.
```swift
let decoder = CSVDecoder {
@@ -420,7 +420,7 @@ let decoder = CSVDecoder {
```swift
let encoder = CSVEncoder()
-let data: Data = try encoder.encode(value)
+let data = try encoder.encode(value, into: Data.self)
```
The `Encoder`'s `encode()` function creates a CSV file as a `Data` blob, a `String`, or an actual file in the file system.
@@ -472,6 +472,45 @@ encoder.dataStrategy = .custom { (data, encoder) in
> The `.headers` configuration is required if you are using keyed encoding container.
+
+
+CSVEncoder.LazyEncoder
.
+
+A series of codable types can be encoded _on demand_ with the encoder's `lazy(into:)` function.
+
+```swift
+let lazyEncoder = CSVEncoder().lazy(into: Data.self)
+for student in students {
+ try lazyEncoder.encode(student)
+}
+let data = try lazyEncoder.endEncoding()
+```
+
+Call `endEncoding()` once there is no more values to be encoded. The function will return the encoded CSV.
+```swift
+let lazyEncoder = CSVEncoder().lazy(into: String.self)
+students.forEach {
+ try lazyEncoder.encode($0)
+}
+let string = try lazyEncoder.endEncoding()
+```
+
+A nice benefit of using the _lazy_ operation, is that it lets you switch how a row is encoded at any point. For example:
+```swift
+let lazyEncoder = CSVEncoder(configuration: config).lazy(into: fileURL)
+students.forEach { try lazyEncoder.encode($0) }
+teachers.forEach { try lazyEncoder.encode($0) }
+try lazyEncoder.endEncoding()
+```
+
+Since `LazyEncoder` exclusively provides sequential encoding; setting the buffering strategy to `.sequential` will reduce the encoder's memory usage.
+
+```swift
+let lazyEncoder = CSVEncoder {
+ $0.bufferingStrategy = .sequential
+}.lazy(into: String.self)
+```
+
diff --git a/sources/Delimiter.swift b/sources/Delimiter.swift
index c3b27ca..9e0de31 100644
--- a/sources/Delimiter.swift
+++ b/sources/Delimiter.swift
@@ -7,7 +7,7 @@ public enum Delimiter {
}
extension Delimiter {
- /// The delimiter between fields/vlaues.
+ /// The delimiter between fields/values.
public struct Field: ExpressibleByNilLiteral, ExpressibleByStringLiteral, RawRepresentable {
public let rawValue: String.UnicodeScalarView
diff --git a/sources/Deprecated.swift b/sources/Deprecated.swift
index 8c3e285..93f9203 100644
--- a/sources/Deprecated.swift
+++ b/sources/Deprecated.swift
@@ -75,4 +75,21 @@ extension CSVWriter {
public static func serialize(row: S, into fileURL: URL, append: Bool, setter: (_ configuration: inout Configuration) -> Void) throws where S.Element==C, C.Element==String {
try self.encode(rows: row, into: fileURL, append: append, setter: setter)
}
+
+ @available(*, deprecated, renamed: "endEncoding()")
+ public func endFile() throws {
+ try self.endEncoding()
+ }
+}
+
+extension CSVEncoder {
+ @available(*, deprecated, renamed: "CSVDecoder.LazyDecoder")
+ typealias LazySequence = CSVDecoder.LazyDecoder
+}
+
+extension CSVEncoder {
+ @available(*, deprecated, renamed: "encode(_:into:)")
+ open func encode(_ value: T) throws -> Data {
+ try self.encode(value, into: Data.self)
+ }
}
diff --git a/sources/declarative/decodable/Decoder.swift b/sources/declarative/decodable/Decoder.swift
index 7cfd262..f6f7f91 100644
--- a/sources/declarative/decodable/Decoder.swift
+++ b/sources/declarative/decodable/Decoder.swift
@@ -69,31 +69,31 @@ extension CSVDecoder {
}
extension CSVDecoder {
- /// Returns a sequence for decoding each row from a CSV file (given as a `Data` blob).
+ /// Returns a sequence for decoding row-by-row from a CSV file (given as a `Data` blob).
/// - parameter data: The data blob representing a CSV file.
/// - throws: `CSVError` exclusively.
- open func lazy(from data: Data) throws -> LazySequence {
+ open func lazy(from data: Data) throws -> LazyDecoder {
let reader = try CSVReader(input: data, configuration: self._configuration.readerConfiguration)
let source = ShadowDecoder.Source(reader: reader, configuration: self._configuration, userInfo: self.userInfo)
- return LazySequence(source: source)
+ return LazyDecoder(source: source)
}
- /// Returns a sequence for decoding each row from a CSV file (given as a `String`).
+ /// Returns a sequence for decoding row-by-row from a CSV file (given as a `String`).
/// - parameter string: A Swift string representing a CSV file.
/// - throws: `CSVError` exclusively.
- open func lazy(from string: String) throws -> LazySequence {
+ open func lazy(from string: String) throws -> LazyDecoder {
let reader = try CSVReader(input: string, configuration: self._configuration.readerConfiguration)
let source = ShadowDecoder.Source(reader: reader, configuration: self._configuration, userInfo: self.userInfo)
- return LazySequence(source: source)
+ return LazyDecoder(source: source)
}
- /// Returns a sequence for decoding each row from a CSV file (being pointed by `url`).
+ /// Returns a sequence for decoding row-by-row from a CSV file (being pointed by `url`).
/// - parameter url: The URL pointing to the file to decode.
/// - throws: `CSVError` exclusively.
- open func lazy(from url: URL) throws -> LazySequence {
+ open func lazy(from url: URL) throws -> LazyDecoder {
let reader = try CSVReader(input: url, configuration: self._configuration.readerConfiguration)
let source = ShadowDecoder.Source(reader: reader, configuration: self._configuration, userInfo: self.userInfo)
- return LazySequence(source: source)
+ return LazyDecoder(source: source)
}
}
diff --git a/sources/declarative/decodable/DecoderLazy.swift b/sources/declarative/decodable/DecoderLazy.swift
index f331474..90cec08 100644
--- a/sources/declarative/decodable/DecoderLazy.swift
+++ b/sources/declarative/decodable/DecoderLazy.swift
@@ -1,20 +1,47 @@
extension CSVDecoder {
- /// Swift sequence type giving access to all the "undecoded" CSV rows.
+ /// Lazy decoder allowing declarative row-by-row decoding.
///
/// The CSV rows are read _on-demand_ and only decoded when explicitly told so (unlike the default _decode_ functions).
- public struct LazySequence: IteratorProtocol, Sequence {
+ public final class LazyDecoder: IteratorProtocol, Sequence {
/// The source of the CSV data.
private let _source: ShadowDecoder.Source
/// The row to be read (not decoded) next.
- private var _currentIndex: Int = 0
+ private var _currentIndex: Int
+ /// A dictionary you use to customize the decoding process by providing contextual information.
+ public var userInfo: [CodingUserInfoKey:Any] { self._source.userInfo }
+
/// Designated initalizer passing all the required components.
/// - parameter source: The data source for the decoder.
internal init(source: ShadowDecoder.Source) {
self._source = source
+ self._currentIndex = 0
+ }
+
+ /// Returns a value of the type you specify, decoded from a CSV row.
+ ///
+ /// This function will throw an error if the file has reached the end. If you are unsure where the CSV file ends, use the `next()` function instead.
+ /// - parameter type: The type of the value to decode from the supplied file.
+ /// - returns: A CSV row decoded as a type `T`.
+ public func decode(_ type: T.Type) throws -> T {
+ guard let rowDecoder = self.next() else { throw CSVDecoder.Error._unexpectedEnd() }
+ return try rowDecoder.decode(type)
+ }
+
+ /// Returns a value of the type you specify, decoded from a CSV row (if there are still rows to be decoded in the file).
+ /// - parameter type: The type of the value to decode from the supplied file.
+ /// - returns: A CSV row decoded as a type `T` or `nil` if the CSV file doesn't contain any more rows.
+ public func decodeIfPresent(_ type: T.Type) throws -> T? {
+ guard let rowDecoder = self.next() else { return nil }
+ return try rowDecoder.decode(type)
+ }
+
+ /// Ignores the subsequent row.
+ public func ignoreRow() {
+ let _ = self.next()
}
- /// Advances to the next row and returns a `LazySequence.Row`, or `nil` if no next row exists.
- public mutating func next() -> RowDecoder? {
+ /// Advances to the next row and returns a `LazyDecoder.Row`, or `nil` if no next row exists.
+ public func next() -> RowDecoder? {
guard !self._source.isRowAtEnd(index: self._currentIndex) else { return nil }
defer { self._currentIndex += 1 }
@@ -24,7 +51,7 @@ extension CSVDecoder {
}
}
-extension CSVDecoder.LazySequence {
+extension CSVDecoder.LazyDecoder {
/// Pointer to a row within a CSV file that is able to decode it to a custom type.
public struct RowDecoder {
/// The representation of the decoding process point-in-time.
@@ -36,10 +63,21 @@ extension CSVDecoder.LazySequence {
self._decoder = decoder
}
- /// Returns a value of the type you specify, decoded from CSV row.
+ /// Returns a value of the type you specify, decoded from a CSV row.
/// - parameter type: The type of the value to decode from the supplied file.
@inline(__always) public func decode(_ type: T.Type) throws -> T {
return try T(from: self._decoder)
}
}
}
+
+// MARK: -
+
+fileprivate extension CSVDecoder.Error {
+ /// Error raised when the end of the file has been reached unexpectedly.
+ static func _unexpectedEnd() -> CSVError {
+ .init(.invalidPath,
+ reason: "There are no more rows to decode. The file is at the end.",
+ help: "Use next() or decodeIfPresent(_:) instead of decode(_:) if you are unsure where the file ends.")
+ }
+}
diff --git a/sources/declarative/encodable/Encoder.swift b/sources/declarative/encodable/Encoder.swift
index 680ae66..b65323e 100644
--- a/sources/declarative/encodable/Encoder.swift
+++ b/sources/declarative/encodable/Encoder.swift
@@ -33,8 +33,9 @@ import Foundation
extension CSVEncoder {
/// Returns a CSV-encoded representation of the value you supply.
/// - parameter value: The value to encode as CSV.
+ /// - parameter type: The Swift type for a data blob.
/// - returns: `Data` blob with the CSV representation of `value`.
- open func encode(_ value: T) throws -> Data {
+ open func encode(_ value: T, into type: Data.Type) throws -> Data {
let writer = try CSVWriter(configuration: self._configuration.writerConfiguration)
let sink = try ShadowEncoder.Sink(writer: writer, configuration: self._configuration, userInfo: self.userInfo)
try value.encode(to: ShadowEncoder(sink: sink, codingPath: []))
@@ -44,9 +45,10 @@ extension CSVEncoder {
/// Returns a CSV-encoded representation of the value you supply.
/// - parameter value: The value to encode as CSV.
+ /// - parameter type: The Swift type for a string.
/// - returns: `String` with the CSV representation of `value`.
- open func encode(_ value: T, into: String.Type) throws -> String {
- let data = try self.encode(value)
+ open func encode(_ value: T, into type: String.Type) throws -> String {
+ let data = try self.encode(value, into: Data.self)
let encoding = self._configuration.writerConfiguration.encoding ?? .utf8
return String(data: data, encoding: encoding)!
}
@@ -63,6 +65,36 @@ extension CSVEncoder {
}
}
+extension CSVEncoder {
+ /// Returns an instance to encode row-by-row the feeded values.
+ /// - parameter type: The Swift type for a data blob.
+ /// - returns: Instance used for _on demand_ encoding.
+ open func lazy(into type: Data.Type) throws -> LazyEncoder {
+ let writer = try CSVWriter(configuration: self._configuration.writerConfiguration)
+ let sink = try ShadowEncoder.Sink(writer: writer, configuration: self._configuration, userInfo: self.userInfo)
+ return LazyEncoder(sink: sink)
+ }
+
+ /// Returns an instance to encode row-by-row the feeded values.
+ /// - parameter type: The Swift type for a data blob.
+ /// - returns: Instance used for _on demand_ encoding.
+ open func lazy(into type: String.Type) throws -> LazyEncoder {
+ let writer = try CSVWriter(configuration: self._configuration.writerConfiguration)
+ let sink = try ShadowEncoder.Sink(writer: writer, configuration: self._configuration, userInfo: self.userInfo)
+ return LazyEncoder(sink: sink)
+ }
+
+ /// Returns an instance to encode row-by-row the feeded values.
+ /// - parameter fileURL: The file receiving the encoded values.
+ /// - parameter append: In case an existing file is under the given URL, this Boolean indicates that the information will be appended to the file (`true`), or the file will be overwritten (`false`).
+ /// - returns: Instance used for _on demand_ encoding.
+ open func lazy(into fileURL: URL, append: Bool = false) throws -> LazyEncoder {
+ let writer = try CSVWriter(fileURL: fileURL, append: append, configuration: self._configuration.writerConfiguration)
+ let sink = try ShadowEncoder.Sink(writer: writer, configuration: self._configuration, userInfo: self.userInfo)
+ return LazyEncoder(sink: sink)
+ }
+}
+
#if canImport(Combine)
import Combine
diff --git a/sources/declarative/encodable/EncoderLazy.swift b/sources/declarative/encodable/EncoderLazy.swift
new file mode 100644
index 0000000..d6e917c
--- /dev/null
+++ b/sources/declarative/encodable/EncoderLazy.swift
@@ -0,0 +1,81 @@
+import Foundation
+
+extension CSVEncoder {
+ /// Lazy encoder allowing declarative row-by-row encoding.
+ public final class LazyEncoder {
+ /// The sink of the CSV data.
+ private let _sink: ShadowEncoder.Sink
+ /// The row to be written next.
+ private var _currentIndex: Int
+ /// A dictionary you use to customize the encoding process by providing contextual information.
+ public var userInfo: [CodingUserInfoKey:Any] { self._sink.userInfo }
+
+ /// Designated initializer passing all the required components.
+ /// - parameter sink: The data _gatherer_ for the encoder.
+ internal init(sink: ShadowEncoder.Sink) {
+ self._sink = sink
+ self._currentIndex = 0
+ }
+
+ /// Encodes the given value as a CSV row.
+ /// - parameter value: The value to encode as CSV.
+ public func encode(_ value: T) throws {
+ let encoder = ShadowEncoder(sink: self._sink, codingPath: [IndexKey(self._currentIndex)])
+ try value.encode(to: encoder)
+ self._currentIndex += 1
+ }
+
+ /// Encodes a row where all its fields are empty.
+ public func encodeEmptyRow() throws {
+ let numFields = self._sink.numExpectedFields
+ guard numFields > 0 else { throw CSVEncoder.Error._invalidRowCompletionOnEmptyFile() }
+
+ let encoder = ShadowEncoder(sink: self._sink, codingPath: [IndexKey(self._currentIndex)])
+ var container = ShadowEncoder.UnkeyedContainer(unsafeEncoder: encoder, rowIndex: self._currentIndex)
+ for _ in 0.. Data {
+ try self._sink.completeEncoding()
+ return try self._sink.data()
+ }
+}
+
+extension CSVEncoder.LazyEncoder where Outcome==String {
+ /// Finish the encoding process and returns the CSV (as a string).
+ ///
+ /// Calls to `encode(_:)` after this function will throw an error.
+ public func endEncoding() throws -> String {
+ try self._sink.completeEncoding()
+ let data = try self._sink.data()
+ let encoding = self._sink.configuration.encoding ?? .utf8
+ return String(data: data, encoding: encoding)!
+ }
+}
+
+extension CSVEncoder.LazyEncoder where Outcome==URL {
+ /// Finish the encoding process and closes the output file.
+ ///
+ /// Calls to `encode(_:)` after this function will throw an error.
+ public func endEncoding() throws {
+ try self._sink.completeEncoding()
+ }
+}
+
+// MARK: -
+
+fileprivate extension CSVEncoder.Error {
+ /// Error raised when a row is ended, but nothing has been written before.
+ static func _invalidRowCompletionOnEmptyFile() -> CSVError {
+ .init(.invalidPath,
+ reason: "An empty row cannot be encoded if the number of fields hold by the CSV file is unkwnown.",
+ help: "Specify a headers row or encode a row with content before encoding an empty row.")
+ }
+}
diff --git a/sources/declarative/encodable/internal/Sink.swift b/sources/declarative/encodable/internal/Sink.swift
index 800b6fc..659d38e 100644
--- a/sources/declarative/encodable/internal/Sink.swift
+++ b/sources/declarative/encodable/internal/Sink.swift
@@ -1,3 +1,5 @@
+import Foundation
+
internal extension ShadowEncoder {
/// Sink of all CSV data.
final class Sink {
@@ -106,6 +108,10 @@ internal extension ShadowEncoder {
}
}
+
+ deinit {
+ try? self.completeEncoding()
+ }
}
}
@@ -188,7 +194,14 @@ extension ShadowEncoder.Sink {
}
}
// 10. Finish the file.
- try self._writer.endFile()
+ try self._writer.endEncoding()
+ }
+
+ /// Returns the generated blob of data if the `_writer` was initialized with a memory position (i.e. `String` or `Data`, but not a file nor a network socket).
+ /// - remark: Please notice that the `endEncoding()` function must be called before this function is used.
+ /// - throws: `CSVError` exclusively.
+ public func data() throws -> Data {
+ try self._writer.data()
}
}
diff --git a/sources/declarative/encodable/internal/SinkBuffer.swift b/sources/declarative/encodable/internal/SinkBuffer.swift
index c49bbb7..4a9a21e 100644
--- a/sources/declarative/encodable/internal/SinkBuffer.swift
+++ b/sources/declarative/encodable/internal/SinkBuffer.swift
@@ -3,7 +3,7 @@ extension ShadowEncoder.Sink {
internal final class Buffer {
/// The buffering strategy.
let strategy: Strategy.EncodingBuffer
- /// The number of expectedFields
+ /// The number of expectedFields.
private let _expectedFields: Int
/// The underlying storage.
private var _storage: [Int: [Int:String]]
diff --git a/sources/imperative/writer/Writer.swift b/sources/imperative/writer/Writer.swift
index 0044ec6..de568d2 100644
--- a/sources/imperative/writer/Writer.swift
+++ b/sources/imperative/writer/Writer.swift
@@ -47,11 +47,11 @@ public final class CSVWriter {
}
deinit {
- try? self.endFile()
+ try? self.endEncoding()
}
/// Returns the generated blob of data if the writer was initialized with a memory position (i.e. `String` or `Data`, but not a file nor a network socket).
- /// - remark: Please notice that the `endFile()` function must be called before this function is used.
+ /// - remark: Please notice that the `endEncoding()` function must be called before this function is used.
/// - throws: `CSVError` exclusively.
public func data() throws -> Data {
guard case .closed = self._stream.streamStatus else {
@@ -69,7 +69,7 @@ public final class CSVWriter {
extension CSVWriter {
/// Finishes the file and closes the output stream (if not indicated otherwise in the initializer).
/// - throws: `CSVError` exclusively.
- public func endFile() throws {
+ public func endEncoding() throws {
guard self._stream.streamStatus != .closed else { return }
if self.fieldIndex > 0 {
@@ -259,11 +259,10 @@ fileprivate extension CSVWriter.Error {
help: "Always write the same amount of fields per row.",
userInfo: ["Number of expected fields per row": expectedFields])
}
-
/// Error raised when a row is ended, but nothing has been written before.
static func _invalidRowCompletionOnEmptyFile() -> CSVError {
.init(.invalidOperation,
- reason: "An empty row cannot be writen if the number of fields hold by the file is unkwnown.",
+ reason: "An empty row cannot be writen if the number of fields hold by the CSV file is unkwnown.",
help: "Write a headers row or a row with content before writing an empty row.")
}
/// Error raised when the data was accessed before the stream was closed.
@@ -272,7 +271,7 @@ fileprivate extension CSVWriter.Error {
static func _invalidDataAccess(status: Stream.Status, error: Swift.Error?) -> CSVError {
.init(.invalidOperation, underlying: error,
reason: "The memory stream must be closed before the data can be accessed.",
- help: "Call endFile() before accessing the data. Also remember, that only Data and String initializers can access memory data.",
+ help: "Call endEncoding() before accessing the data. Also remember, that only Data and String initializers can access memory data.",
userInfo: ["Stream status": status])
}
@@ -281,6 +280,6 @@ fileprivate extension CSVWriter.Error {
static func _dataFailed(error: Swift.Error?) -> CSVError {
.init(.streamFailure, underlying: error,
reason: "The stream failed to returned the encoded data.",
- help: "Call endFile() before accessing the data. Also remember, that only Data and String initializers can access memory data.")
+ help: "Call endEncoding() before accessing the data. Also remember, that only Data and String initializers can access memory data.")
}
}
diff --git a/sources/imperative/writer/WriterAPI.swift b/sources/imperative/writer/WriterAPI.swift
index 11178c4..25296d6 100644
--- a/sources/imperative/writer/WriterAPI.swift
+++ b/sources/imperative/writer/WriterAPI.swift
@@ -4,7 +4,7 @@ extension CSVWriter {
/// Creates a writer instance that will be used to encode CSV-formatted data.
///
/// The output data can be accessed at the end of the encoding process through the `CSVWriter`'s `data()` function.
- /// Make sure `endFile()` is called before requesting `data()`.
+ /// Make sure `endEncoding()` is called before requesting `data()`.
/// - parameter configuration: Configuration values specifying how the CSV output should look like.
/// - throws: `CSVError` exclusively.
public convenience init(configuration: Configuration = .init()) throws {
@@ -20,7 +20,7 @@ extension CSVWriter {
/// Creates a writer instance that will be used to encode CSV-formatted data into a file pointed by the given URL.
///
- /// Make sure `endFile()` is called at the end of the encoding process.
+ /// Make sure `endEncoding()` is called at the end of the encoding process.
/// - parameter fileURL: The URL pointing to the targeted file.
/// - parameter append: In case an existing file is under the given URL, this Boolean indicates that the information will be appended to the file (`true`), or the file will be overwritten (`false`).
/// - parameter configuration: Configuration values specifying how the CSV output should look like.
@@ -76,7 +76,7 @@ extension CSVWriter {
try writer.write(row: row)
}
- try writer.endFile()
+ try writer.endEncoding()
return try writer.data()
}
@@ -103,7 +103,7 @@ extension CSVWriter {
try writer.write(row: row)
}
- try writer.endFile()
+ try writer.endEncoding()
}
}
diff --git a/tests/ActiveTests/WriterTests.swift b/tests/ActiveTests/WriterTests.swift
index 54258b3..6890128 100644
--- a/tests/ActiveTests/WriterTests.swift
+++ b/tests/ActiveTests/WriterTests.swift
@@ -41,14 +41,14 @@ extension WriterTests {
func testEmpty() throws {
// 1. Tests the data encoder.
let dataWriter = try CSVWriter()
- try dataWriter.endFile()
+ try dataWriter.endEncoding()
let data = try dataWriter.data()
XCTAssertTrue(data.isEmpty)
// 2. Tests the file encoder.
let url = _TestData.generateTemporaryFileURL()
XCTAssertTrue(FileManager.default.createFile(atPath: url.path, contents: data))
let fileWriter = try CSVWriter(fileURL: url)
- try fileWriter.endFile()
+ try fileWriter.endEncoding()
try FileManager.default.removeItem(at: url)
}
@@ -75,7 +75,7 @@ extension WriterTests {
let writer = try CSVWriter()
try writer.write(field: field)
try writer.endRow()
- try writer.endFile()
+ try writer.endEncoding()
XCTAssertEqual(data, try writer.data())
}
@@ -151,7 +151,7 @@ extension WriterTests {
try writer.write(row: row)
}
- try writer.endFile()
+ try writer.endEncoding()
let result = try writer.data()
let data = _TestData.toCSV(input, delimiters: (",", "\n")).data(using: .utf8)!
@@ -166,7 +166,7 @@ extension WriterTests {
try writer.write(row: ["one", "two", "three"])
try writer.write(fields: ["four", "five", "six"])
try writer.endRow()
- try writer.endFile()
+ try writer.endEncoding()
try FileManager.default.removeItem(at: fileURL)
}
@@ -178,7 +178,7 @@ extension WriterTests {
try writer.write(fields: ["four", "five", "six", "seven"])
XCTFail("The previous line shall throw an error")
} catch {
- try writer.endFile()
+ try writer.endEncoding()
}
}
@@ -188,7 +188,7 @@ extension WriterTests {
try writer.writeEmptyRow()
try writer.write(row: ["four", "five", "six"])
try writer.writeEmptyRow()
- try writer.endFile()
+ try writer.endEncoding()
}
/// Tests writing empty rows when the number of fields are unknown.
@@ -198,7 +198,7 @@ extension WriterTests {
try writer.writeEmptyRow()
XCTFail("The previous line shall throw an error")
} catch {
- try writer.endFile()
+ try writer.endEncoding()
}
}
}
diff --git a/tests/CodableTests/EncodingLazyTests.swift b/tests/CodableTests/EncodingLazyTests.swift
new file mode 100644
index 0000000..8f1cbaa
--- /dev/null
+++ b/tests/CodableTests/EncodingLazyTests.swift
@@ -0,0 +1,282 @@
+import XCTest
+import CodableCSV
+
+/// Tests checking the lazy encoding operation.
+final class EncodingLazyTests: XCTestCase {
+ override func setUp() {
+ self.continueAfterFailure = false
+ }
+}
+
+extension EncodingLazyTests {
+ /// Test data used throughout this `XCTestCase`.
+ private enum _TestData {
+ struct KeyedStudent: Encodable {
+ var name: String
+ var age: Int
+ var country: String
+ var hasPet: Bool
+ }
+
+ struct UnkeyedStudent: Encodable {
+ var name: String
+ var age: Int
+ var country: String
+ var hasPet: Bool
+
+ func encode(to encoder: Encoder) throws {
+ var container = encoder.unkeyedContainer()
+ try container.encode(self.name)
+ try container.encode(self.age)
+ try container.encode(self.country)
+ try container.encode(self.hasPet)
+ }
+ }
+ }
+}
+
+// MARK: -
+
+extension EncodingLazyTests {
+ /// Test the encoding of an empty CSV file.
+ func testEmptyFile() throws {
+ // The configuration values to be tests.
+ let encoding: String.Encoding = .utf8
+ let bomStrategy: Strategy.BOM = .never
+ let delimiters: Delimiter.Pair = (",", "\n")
+ let bufferStrategies: [Strategy.EncodingBuffer] = [.keepAll, .assembled, .sequential]
+
+ for s in bufferStrategies {
+ var configuration = CSVEncoder.Configuration()
+ configuration.encoding = encoding
+ configuration.bomStrategy = bomStrategy
+ configuration.delimiters = delimiters
+ configuration.bufferingStrategy = s
+
+ let lazyEncoder = try CSVEncoder(configuration: configuration).lazy(into: Data.self)
+ let data = try lazyEncoder.endEncoding()
+ XCTAssertTrue(data.isEmpty)
+ }
+ }
+
+ /// Tests the encoding of a single empty field in a CSV file.
+ func testSingleEmptyField() throws {
+ // The configuration values to be tests.
+ let encoding: String.Encoding = .utf8
+ let bomStrategy: Strategy.BOM = .never
+ let delimiters: Delimiter.Pair = (",", "\n")
+ let bufferStrategies: [Strategy.EncodingBuffer] = [.keepAll, .assembled, .sequential]
+ // The data used for testing.
+ let value = [String()]
+
+ for s in bufferStrategies {
+ var configuration = CSVEncoder.Configuration()
+ configuration.encoding = encoding
+ configuration.bomStrategy = bomStrategy
+ configuration.delimiters = delimiters
+ configuration.bufferingStrategy = s
+
+ let lazyEncoder = try CSVEncoder(configuration: configuration).lazy(into: String.self)
+ try lazyEncoder.encode(value)
+ let string = try lazyEncoder.endEncoding()
+ XCTAssertEqual(string, "\(delimiters.row.rawValue)")
+ }
+ }
+
+ /// Tests a single custom type encoding (with headers).
+ func testSingleCustomType() throws {
+ // The configuration values to be tests.
+ let encoding: String.Encoding = .utf8
+ let bomStrategy: Strategy.BOM = .never
+ let delimiters: Delimiter.Pair = (",", "\n")
+ let bufferStrategies: [Strategy.EncodingBuffer] = [.keepAll, .assembled, .sequential]
+ let headers = ["name", "age", "country", "hasPet"]
+ // The data used for testing.
+ let student = _TestData.KeyedStudent(name: "Marcos", age: 111, country: "Spain", hasPet: true)
+
+ for s in bufferStrategies {
+ var configuration = CSVEncoder.Configuration()
+ configuration.encoding = encoding
+ configuration.bomStrategy = bomStrategy
+ configuration.delimiters = delimiters
+ configuration.bufferingStrategy = s
+ configuration.headers = headers
+
+ let lazyEncoder = try CSVEncoder(configuration: configuration).lazy(into: String.self)
+ try lazyEncoder.encode(student)
+ let string = try lazyEncoder.endEncoding()
+ let content = string.split(separator: .init(delimiters.row.rawValue.first!))
+
+ let encodedHeaders = content[0].split(separator: Character(delimiters.field.rawValue.first!)).map { String($0) }
+ XCTAssertEqual(headers, encodedHeaders)
+
+ let encodedValues = content[1].split(separator: Character(delimiters.field.rawValue.first!)).map { String($0) }
+ XCTAssertEqual(student.name, encodedValues[0])
+ XCTAssertEqual(String(student.age), encodedValues[1])
+ XCTAssertEqual(String(student.country), encodedValues[2])
+ XCTAssertEqual(String(student.hasPet), encodedValues[3])
+ }
+ }
+
+ /// Tests a single custom type encoding (with NO headers).
+ func testSingleCustomTypeNoHeaders() throws {
+ // The configuration values to be tests.
+ let encoding: String.Encoding = .utf8
+ let bomStrategy: Strategy.BOM = .never
+ let delimiters: Delimiter.Pair = (",", "\n")
+ let bufferStrategies: [Strategy.EncodingBuffer] = [.keepAll, .assembled, .sequential]
+ let headers: [String] = []
+ // The data used for testing.
+ let student = _TestData.UnkeyedStudent(name: "Marcos", age: 111, country: "Spain", hasPet: true)
+
+ for s in bufferStrategies {
+ var configuration = CSVEncoder.Configuration()
+ configuration.encoding = encoding
+ configuration.bomStrategy = bomStrategy
+ configuration.delimiters = delimiters
+ configuration.bufferingStrategy = s
+ configuration.headers = headers
+
+ let lazyEncoder = try CSVEncoder(configuration: configuration).lazy(into: String.self)
+ try lazyEncoder.encode(student)
+ let string = try lazyEncoder.endEncoding()
+ let content = string.split(separator: .init(delimiters.row.rawValue.first!))
+
+ let encodedValues = content[0].split(separator: Character(delimiters.field.rawValue.first!)).map { String($0) }
+ XCTAssertEqual(student.name, encodedValues[0])
+ XCTAssertEqual(String(student.age), encodedValues[1])
+ XCTAssertEqual(String(student.country), encodedValues[2])
+ XCTAssertEqual(String(student.hasPet), encodedValues[3])
+ }
+ }
+
+ /// Tests multiple custom types encoding.
+ func testMultipleKeyed() throws {
+ // The configuration values to be tests.
+ let encoding: String.Encoding = .utf8
+ let bomStrategy: Strategy.BOM = .never
+ let delimiters: Delimiter.Pair = (",", "\n")
+ let bufferStrategies: [Strategy.EncodingBuffer] = [.keepAll, .assembled, .sequential]
+ let headers = ["name", "age", "country", "hasPet"]
+ // The data used for testing.
+ let students: [_TestData.KeyedStudent] = [
+ .init(name: "Marcos", age: 1, country: "Spain", hasPet: true),
+ .init(name: "Anaïs", age: 2, country: "France", hasPet: false),
+ .init(name: "Alex", age: 3, country: "Canada", hasPet: false),
+ .init(name: "家豪", age: 4, country: "China", hasPet: true),
+ .init(name: "Дэниел", age: 5, country: "Russia", hasPet: true),
+ .init(name: "ももこ", age: 6, country: "Japan", hasPet: false)
+ ]
+
+ for s in bufferStrategies {
+ let encoder = CSVEncoder()
+ encoder.encoding = encoding
+ encoder.bomStrategy = bomStrategy
+ encoder.delimiters = delimiters
+ encoder.bufferingStrategy = s
+ encoder.headers = headers
+
+ let lazyEncoder = try encoder.lazy(into: String.self)
+ for student in students {
+ try lazyEncoder.encode(student)
+ }
+
+ let string = try lazyEncoder.endEncoding()
+ let content = string.split(separator: Character(delimiters.row.rawValue.first!)).map { String($0) }
+ XCTAssertEqual(content.count, 1+students.count)
+ XCTAssertEqual(content[0], headers.joined(separator: String(delimiters.field.rawValue)))
+ }
+ }
+
+ /// Tests multiple custom types encoding (with headers).
+ func testMultipleUnkeyed() throws {
+ // The configuration values to be tests.
+ let encoding: String.Encoding = .utf8
+ let bomStrategy: Strategy.BOM = .never
+ let delimiters: Delimiter.Pair = (",", "\n")
+ let bufferStrategies: [Strategy.EncodingBuffer] = [.keepAll, .assembled, .sequential]
+ let headers = [[], ["name", "age", "country", "hasPet"]]
+ // The data used for testing.
+ let students: [_TestData.UnkeyedStudent] = [
+ .init(name: "Marcos", age: 1, country: "Spain", hasPet: true),
+ .init(name: "Anaïs", age: 2, country: "France", hasPet: false),
+ .init(name: "Alex", age: 3, country: "Canada", hasPet: false),
+ .init(name: "家豪", age: 4, country: "China", hasPet: true),
+ .init(name: "Дэниел", age: 5, country: "Russia", hasPet: true),
+ .init(name: "ももこ", age: 6, country: "Japan", hasPet: false)
+ ]
+
+ for s in bufferStrategies {
+ for h in headers {
+ let encoder = CSVEncoder()
+ encoder.encoding = encoding
+ encoder.bomStrategy = bomStrategy
+ encoder.delimiters = delimiters
+ encoder.bufferingStrategy = s
+ encoder.headers = h
+
+ let lazyEncoder = try encoder.lazy(into: String.self)
+ for student in students {
+ try lazyEncoder.encode(student)
+ }
+ let string = try lazyEncoder.endEncoding()
+ XCTAssertFalse(string.isEmpty)
+
+ let content = string.split(separator: Character(delimiters.row.rawValue.first!)).map { String($0) }
+ if h.isEmpty {
+ XCTAssertEqual(content.count, students.count)
+ } else {
+ XCTAssertEqual(content.count, 1+students.count)
+ XCTAssertEqual(content[0], h.joined(separator: String(delimiters.field.rawValue)))
+ }
+ }
+ }
+ }
+
+ /// Tests multiple custom types encoding (with headers).
+ func testEncodingEmptyRows() throws {
+ // The configuration values to be tests.
+ let encoding: String.Encoding = .utf8
+ let bomStrategy: Strategy.BOM = .never
+ let delimiters: Delimiter.Pair = (",", "\n")
+ let bufferStrategies: [Strategy.EncodingBuffer] = [/*.keepAll, .assembled, */.sequential]
+ let headers = [[], ["name", "age", "country", "hasPet"]]
+ // The data used for testing.
+ let studentsA: [_TestData.UnkeyedStudent] = [
+ .init(name: "Marcos", age: 1, country: "Spain", hasPet: true),
+ .init(name: "Anaïs", age: 2, country: "France", hasPet: false)
+ ]
+
+ let studentsB: [_TestData.UnkeyedStudent] = [
+ .init(name: "Alex", age: 3, country: "Canada", hasPet: false),
+ .init(name: "家豪", age: 4, country: "China", hasPet: true),
+ .init(name: "Дэниел", age: 5, country: "Russia", hasPet: true),
+ .init(name: "ももこ", age: 6, country: "Japan", hasPet: false)
+ ]
+
+ for s in bufferStrategies {
+ for h in headers {
+ let encoder = CSVEncoder()
+ encoder.encoding = encoding
+ encoder.bomStrategy = bomStrategy
+ encoder.delimiters = delimiters
+ encoder.bufferingStrategy = s
+ encoder.headers = h
+
+ let lazyEncoder = try encoder.lazy(into: String.self)
+ for student in studentsA {
+ try lazyEncoder.encode(student)
+ }
+ try lazyEncoder.encodeEmptyRow()
+ for student in studentsB {
+ try lazyEncoder.encode(student)
+ }
+
+ let string = try lazyEncoder.endEncoding()
+ XCTAssertFalse(string.isEmpty)
+
+ print(string)
+ }
+ }
+ }
+}
diff --git a/tests/CodableTests/EncodingRegularUsageTests.swift b/tests/CodableTests/EncodingRegularUsageTests.swift
index 2439e7e..7712169 100644
--- a/tests/CodableTests/EncodingRegularUsageTests.swift
+++ b/tests/CodableTests/EncodingRegularUsageTests.swift
@@ -75,7 +75,7 @@ extension EncodingRegularUsageTests {
// MARK: -
extension EncodingRegularUsageTests {
- /// Tests the encoding of an empty.
+ /// Tests the encoding of an empty CSV file.
func testEmptyFile() throws {
// The configuration values to be tests.
let encoding: String.Encoding = .utf8
@@ -93,7 +93,7 @@ extension EncodingRegularUsageTests {
configuration.bufferingStrategy = s
let encoder = CSVEncoder(configuration: configuration)
- let data = try encoder.encode(value)
+ let data = try encoder.encode(value, into: Data.self)
XCTAssertTrue(data.isEmpty)
}
}
@@ -106,7 +106,7 @@ extension EncodingRegularUsageTests {
let delimiters: Delimiter.Pair = (",", "\n")
let bufferStrategies: [Strategy.EncodingBuffer] = [.keepAll, .assembled, .sequential]
// The data used for testing.
- let value: [[String]] = [[.init()]]
+ let value = [[String()]]
for s in bufferStrategies {
var configuration = CSVEncoder.Configuration()
@@ -116,7 +116,7 @@ extension EncodingRegularUsageTests {
configuration.bufferingStrategy = s
let encoder = CSVEncoder(configuration: configuration)
- let data = try encoder.encode(value)
+ let data = try encoder.encode(value, into: Data.self)
let string = String(data: data, encoding: encoding)!
XCTAssertEqual(string, "\(delimiters.row.rawValue)")
}
@@ -140,7 +140,7 @@ extension EncodingRegularUsageTests {
configuration.bufferingStrategy = s
let encoder = CSVEncoder(configuration: configuration)
- let data = try encoder.encode(school)
+ let data = try encoder.encode(school, into: Data.self)
XCTAssertTrue(data.isEmpty)
}
}