Skip to content

Commit db4e4b0

Browse files
committed
ShadowEncoder.Sink complete encoding functionality
1 parent 61c26b1 commit db4e4b0

File tree

12 files changed

+238
-38
lines changed

12 files changed

+238
-38
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@ decoder.decimalStratey = .custom {
360360
<ul>
361361
<details><summary>Basic adoption.</summary><p>
362362

363-
`Codable` is just a type alias for `Decodable` and `Encodable`. When a custom type conforms to `Codable`, the type is stating that it has the ability to decode itself from and encode itself to a external representation. Which representation depends on the decoder or encoder chosen. Foundation provides support for [JSON and Property Lists](https://developer.apple.com/documentation/foundation/archives_and_serialization), but the community provide many other formats, such as: [YAML](https://github.com/jpsim/Yams), [XML](https://github.com/MaxDesiatov/XMLCoder), [BSON](https://github.com/OpenKitten/BSON), and CSV (through this library).
363+
When a custom type conforms to `Codable`, the type is stating that it has the ability to decode itself from and encode itself to a external representation. Which representation depends on the decoder or encoder chosen. Foundation provides support for [JSON and Property Lists](https://developer.apple.com/documentation/foundation/archives_and_serialization), but the community provide many other formats, such as: [YAML](https://github.com/jpsim/Yams), [XML](https://github.com/MaxDesiatov/XMLCoder), [BSON](https://github.com/OpenKitten/BSON), and CSV (through this library).
364364

365365
Lets see a regular CSV encoding/decoding usage through `Codable`'s interface. Let's suppose we have a list of students formatted in a CSV file:
366366

@@ -393,7 +393,7 @@ let students = try decoder.decode([Student], from: data)
393393
The inverse process (from Swift to CSV) is very similar (and simple).
394394

395395
```swift
396-
let encoder = CSVEncoder { $0.headerStraty = .firstLine }
396+
let encoder = CSVEncoder { $0.headers = ["name", "age", "hasPet"] }
397397
let newData = try encoder.encode(students)
398398
```
399399

sources/Active/Writer/Writer.swift

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ public final class CSVWriter {
2222
/// The field to write next.
2323
public private(set) var fieldIndex: Int
2424
/// The number of fields per row that are expected.
25+
///
26+
/// It is zero, if no expectectations have been set.
2527
private(set) internal var expectedFields: Int
2628

2729
/// Designated initializer for the CSV writer.
@@ -118,22 +120,24 @@ extension CSVWriter {
118120
/// It is perfectly fine to call this method when only some fields (but not all) have been writen. This function will complete the row writing row delimiters.
119121
/// - throws: `CSVError<CSVWriter>` exclusively.
120122
public func endRow() throws {
123+
// 1. Has any field being writen for the current row? If not, write a complete emtpy row.
121124
guard self.fieldIndex > 0 else {
122125
return try self.writeEmptyRow()
123126
}
124-
127+
// 2. If the number of fields per row is known, write the missing fields (if any).
125128
if self.expectedFields > 0 {
126-
try stride(from: self.fieldIndex, to: self.expectedFields, by: 1).forEach { [f = self.settings.delimiters.field] _ in
127-
try self.lowlevelWrite(delimiter: f)
129+
while self.fieldIndex < self.expectedFields {
130+
try self.lowlevelWrite(delimiter: self.settings.delimiters.field)
128131
try self.lowlevelWrite(field: "")
129132
}
133+
// 3. If the number of fields per row is unknown, store the number of written fields for this row as that number.
130134
} else {
131135
self.expectedFields = self.fieldIndex
132136
}
133-
137+
// 4. Write the row delimiter.
134138
try self.lowlevelWrite(delimiter: self.settings.delimiters.row)
135-
self.rowIndex += 1
136-
self.fieldIndex = 0
139+
// 5. Increment the row index and reset the field index.
140+
(self.rowIndex, self.fieldIndex) = (self.rowIndex + 1, 0)
137141
}
138142
}
139143

sources/Codable/Encodable/Containers/UnkeyedEncodingContainer.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ extension ShadowEncoder.UnkeyedContainer {
7979
return Self(unsafeEncoder: encoder, rowIndex: rowIndex)
8080
case .row:
8181
let error = CSVEncoder.Error.invalidContainerRequest(codingPath: codingPath)
82-
return ShadowEncoder.InvalidContainer<IndexKey>(error: error, encoder: self.encoder)
82+
return ShadowEncoder.InvalidContainer<InvalidKey>(error: error, encoder: self.encoder)
8383
}
8484
}
8585

sources/Codable/Encodable/Encoder.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ extension CSVEncoder {
3838
let writer = try CSVWriter(configuration: self.configuration.writerConfiguration)
3939
let sink = try ShadowEncoder.Sink(writer: writer, configuration: self.configuration, userInfo: self.userInfo)
4040
try value.encode(to: ShadowEncoder(sink: sink, codingPath: []))
41-
try writer.endFile()
41+
try sink.completeEncoding()
4242
return try writer.data()
4343
}
4444

@@ -59,6 +59,6 @@ extension CSVEncoder {
5959
let writer = try CSVWriter(fileURL: fileURL, append: append, configuration: self.configuration.writerConfiguration)
6060
let sink = try ShadowEncoder.Sink(writer: writer, configuration: self.configuration, userInfo: self.userInfo)
6161
try value.encode(to: ShadowEncoder(sink: sink, codingPath: []))
62-
try writer.endFile()
62+
try sink.completeEncoding()
6363
}
6464
}

sources/Codable/Encodable/EncodingStrategy.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,9 @@ extension Strategy {
4949
///
5050
/// Foward encoding jumps are allowed and the user may jump backward to continue encoding.
5151
case unfulfilled
52-
/// No rows are kept in memory and written is performed sequentially.
52+
/// No rows are kept in memory and writes are performed sequentially.
5353
///
54-
/// If a keyed container is used to encode rows and a jump forward is performed all the in-between rows are filled with empty fields.
54+
/// If a keyed container is used to encode rows and a jump forward is requested all the in-between rows are filled with empty fields.
5555
case sequential
5656
}
5757
}
@@ -63,6 +63,8 @@ extension CSVEncoder: Failable {
6363
case invalidConfiguration = 1
6464
/// The encoding coding path is invalid.
6565
case invalidPath = 2
66+
/// An error occurred on the encoder buffer.
67+
case bufferFailure = 4
6668
}
6769

6870
public static var errorDomain: String { "Writer" }
@@ -71,6 +73,7 @@ extension CSVEncoder: Failable {
7173
switch failure {
7274
case .invalidConfiguration: return "Invalid configuration"
7375
case .invalidPath: return "Invalid coding path"
76+
case .bufferFailure: return "Invalid buffer state"
7477
}
7578
}
7679
}

sources/Codable/Encodable/Shadow/ShadowEncoder.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ extension ShadowEncoder {
3838
do { // It is weird that this function (as it's defined in the `Encoder` protocol) doesn't throw. Instead there is just a warning in the function documentation.
3939
return try UnkeyedContainer(encoder: self)
4040
} catch let error {
41-
return InvalidContainer<IndexKey>(error: error, encoder: self)
41+
return InvalidContainer<InvalidKey>(error: error, encoder: self)
4242
}
4343
}
4444

@@ -50,7 +50,7 @@ extension ShadowEncoder {
5050
do { // It is weird that this function (as it's defined in the `Encoder` protocol) doesn't throw. Instead there is just a warning in the function documentation.
5151
return try SingleValueContainer(encoder: self)
5252
} catch let error {
53-
return InvalidContainer<IndexKey>(error: error, encoder: self)
53+
return InvalidContainer<InvalidKey>(error: error, encoder: self)
5454
}
5555
}
5656
}

sources/Codable/Encodable/Shadow/Sink.swift

Lines changed: 59 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ extension ShadowEncoder.Sink {
4545
if rowIndex < self.writer.rowIndex {
4646
return self.writer.expectedFields
4747
} else if rowIndex == self.writer.rowIndex {
48-
return max(self.writer.fieldIndex, self.buffer.fieldCount(forRowIndex: rowIndex))
48+
return max(self.writer.fieldIndex, self.buffer.fieldCount(for: rowIndex))
4949
} else {
50-
return self.buffer.fieldCount(forRowIndex: rowIndex)
50+
return self.buffer.fieldCount(for: rowIndex)
5151
}
5252
}
5353

@@ -70,29 +70,68 @@ extension ShadowEncoder.Sink {
7070

7171
/// Encodes the given field in the given position.
7272
func field(value: String, at rowIndex: Int, _ fieldIndex: Int) throws {
73-
// 1. Check the given row index is matching the row to be written by the writer.
73+
#warning("How to deal with intended field gaps?")
74+
// When the next row is writen, check the previous row.
75+
// Although, what happens when there are several empty rows?
76+
77+
// 1. Is the requested row the same position as the writer's row?
7478
guard self.writer.rowIndex == rowIndex else {
75-
// 1.1. If not, the row must not have been written already (otherwise an error is thrown).
79+
// 1.1. If not, the row must not have been written yet (otherwise an error is thrown).
7680
guard self.writer.rowIndex > rowIndex else { throw CSVEncoder.Error.writingSurpassed(rowIndex: rowIndex, fieldIndex: fieldIndex, value: value) }
7781
// 1.2. If the row hasn't been writen yet, store it in the buffer.
7882
return self.buffer.store(value: value, at: rowIndex, fieldIndex)
7983
}
80-
81-
// 2. Check the field index is matching the field to be written by the writer.
84+
// 2. Is the requested field the same as the writer's field?
8285
guard self.writer.fieldIndex == fieldIndex else {
83-
// 2.1 If not, the field must not have been written already (otherwise an error is thrown).
86+
// 2.1 If not, the field must not have been written yet (otherwise an error is thrown).
8487
guard self.writer.fieldIndex > fieldIndex else { throw CSVEncoder.Error.writingSurpassed(rowIndex: rowIndex, fieldIndex: fieldIndex, value: value) }
8588
// 2.2 If the field hasn't been writen yet, store it in the buffer.
8689
return self.buffer.store(value: value, at: rowIndex, fieldIndex)
8790
}
88-
89-
// 3. This point is only reached if the writer is going to write the provided field next.
91+
// 3. Write the provided field since it is the same as the writer's row/field.
9092
try self.writer.write(field: value)
91-
// 4.
92-
#warning("TODO: Continue here") // Call next() on buffer returning an element and a boolean indicating whether it is the end of the row.
93-
// if self.writer.fieldIndex == self.writer.expectedFields {
94-
// try self.writer.endRow()
95-
// }
93+
// 4. How many fields per row there are? If unknown, stop.
94+
guard self.writer.expectedFields > 0 else { return }
95+
#warning("How to deal with the first ever row when no headers are given?")
96+
while true {
97+
// 5. If is not the end of the row, check the buffer and see whether the following fields are already cached.
98+
while self.writer.fieldIndex < self.writer.expectedFields {
99+
guard let field = self.buffer.retrieveField(at: self.writer.rowIndex, self.writer.fieldIndex) else { return }
100+
try self.writer.write(field: field)
101+
}
102+
// 6. If it is the end of the row, write the row delimiter and pass to the next row.
103+
try self.writer.endRow()
104+
}
105+
}
106+
107+
/// Finishes the whole encoding operation by commiting to the writer any remaining row/field in the buffer.
108+
///
109+
/// This function works even when the number of fields per row are unknown.
110+
func completeEncoding() throws {
111+
// 1. Remove from the buffer the rows/fields from the writer point.
112+
var remainings = self.buffer.retrieveSequence(from: self.writer.rowIndex, fieldIndex: self.writer.fieldIndex)
113+
// 2. After the removal there should be any more rows/fields in the buffer.
114+
guard self.buffer.isEmpty else { throw CSVEncoder.Error.corruptedBuffer() }
115+
// 3. Iterate through all the remaining rows.
116+
while let row = remainings.next() {
117+
// 4. If the writer is further back from the next remaining row. Fill the writer with empty rows.
118+
while self.writer.rowIndex < row.index {
119+
try self.writer.endRow()
120+
}
121+
// 5. Iterate through all the fields in the row.
122+
for field in row.fields {
123+
// 6. If the row is further back from the next remaining field. Fill the writer with empty fields.
124+
while self.writer.fieldIndex < field.index {
125+
try self.writer.write(field: "")
126+
}
127+
// 7. Write the targeted field.
128+
try self.writer.write(field: field.value)
129+
}
130+
// 8. Finish the targeted row.
131+
try self.writer.endRow()
132+
}
133+
// 9. Finish the file.
134+
try self.writer.endFile()
96135
}
97136
}
98137

@@ -124,4 +163,10 @@ fileprivate extension CSVEncoder.Error {
124163
help: "An already written CSV row cannot be rewritten. Be mindful on the encoding order.",
125164
userInfo: ["Row index": rowIndex, "Field index": fieldIndex, "Value": value])
126165
}
166+
/// Error raised when the encoding operation finishes, but there are still values in the buffer.
167+
static func corruptedBuffer() -> CSVError<CSVEncoder> {
168+
.init(.bufferFailure,
169+
reason: "The encoding operation finished, but there were still values in the encoding buffer.",
170+
help: "This should never happen, please contact the repo maintainer sending data with a way to replicate this error.")
171+
}
127172
}

sources/Codable/Encodable/Shadow/SinkBuffer.swift

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ extension ShadowEncoder.Sink {
1313
}
1414

1515
extension ShadowEncoder.Sink.Buffer {
16+
///
17+
var isEmpty: Bool {
18+
#warning("TODO")
19+
fatalError()
20+
}
21+
1622
/// The number of rows being hold by the receiving buffer.
1723
var count: Int {
1824
#warning("TODO")
@@ -22,7 +28,7 @@ extension ShadowEncoder.Sink.Buffer {
2228
/// Returns the number of fields that have been received for the given row.
2329
///
2430
/// If none, it returns *zero*.
25-
func fieldCount(forRowIndex rowIndex: Int) -> Int {
31+
func fieldCount(for rowIndex: Int) -> Int {
2632
#warning("TODO")
2733
fatalError()
2834
}
@@ -32,4 +38,51 @@ extension ShadowEncoder.Sink.Buffer {
3238
#warning("TODO")
3339
fatalError()
3440
}
41+
42+
/// Retrieves and removes from the buffer the indicated value.
43+
func retrieveField(at rowIndex: Int, _ fieldIndex: Int) -> String? {
44+
#warning("TODO")
45+
fatalError()
46+
}
47+
48+
/// Retrieves and removes from the buffer all rows/fields from the given indices.
49+
///
50+
/// This function never returns rows at an index smaller than the passed `rowIndex`. Also, for the `rowIndex`, it doesn't return the fields previous the `fieldIndex`.
51+
func retrieveSequence(from rowIndex: Int, fieldIndex: Int) -> RowSequence {
52+
#warning("TODO")
53+
fatalError()
54+
}
55+
}
56+
57+
extension ShadowEncoder.Sink.Buffer {
58+
///
59+
struct RowSequence: Sequence, IteratorProtocol {
60+
///
61+
mutating func next() -> Row? {
62+
#warning("TODO")
63+
fatalError()
64+
}
65+
66+
var isEmpty: Bool {
67+
#warning("TODO")
68+
fatalError()
69+
}
70+
}
71+
}
72+
73+
extension ShadowEncoder.Sink.Buffer {
74+
///
75+
struct Row {
76+
///
77+
let index: Int
78+
///
79+
let fields: [Field]
80+
}
81+
82+
struct Field {
83+
///
84+
let index: Int
85+
///
86+
let value: String
87+
}
3588
}

tests/CodableTests/DecodingRegularUsageTests.swift

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
import XCTest
2-
@testable import CodableCSV
2+
import CodableCSV
33

4-
/// Tests for the decodable pet store data.
4+
/// Tests checking the regular decoding usage.
55
final class DecodingRegularUsageTests: XCTestCase {
66
override func setUp() {
77
self.continueAfterFailure = false
88
}
99
}
1010

11-
// MARK: -
12-
1311
extension DecodingRegularUsageTests {
1412
/// Test data used throughout this `XCTestCase`.
1513
private enum TestData {

tests/CodableTests/DecodingSinglesTests.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ final class DecodingSinglesTests: XCTestCase {
88
}
99
}
1010

11-
// MARK: -
12-
1311
extension DecodingSinglesTests {
1412
/// Tests the decoding of a completely empty file.
1513
func testEmptyFile() throws {

0 commit comments

Comments
 (0)