Skip to content

Commit 8fcebed

Browse files
committed
First tests for container value types added
1 parent a8e720e commit 8fcebed

File tree

4 files changed

+175
-140
lines changed

4 files changed

+175
-140
lines changed

Sources/Codable/Decodable/Containers/DecodingValue.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,14 @@ extension ShadowDecoder.SingleValueContainer {
172172
let value = try self.decoder.source.field(at: rowIndex, 0)
173173
return try transform(value) ?! DecodingError.typeMismatch(T.self, .invalidTransformation(value: value, codingPath: self.codingPath + [DecodingKey(0)]))
174174
case .file:
175-
throw DecodingError.invalidNestedRequired(codingPath: self.codingPath)
175+
let source = self.decoder.source
176+
// Values are only allowed to be decoded directly from a single value container in "file level" if the CSV file has a single row with a single column.
177+
if source.isRowAtEnd(index: 1), source.numFields == 1 {
178+
let value = try self.decoder.source.field(at: 0, 0)
179+
return try transform(value) ?! DecodingError.typeMismatch(T.self, .invalidTransformation(value: value, codingPath: self.codingPath + [DecodingKey(0), DecodingKey(0)]))
180+
} else {
181+
throw DecodingError.invalidNestedRequired(codingPath: self.codingPath)
182+
}
176183
}
177184
}
178185

Sources/Codable/Decodable/Decoder.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@ open class CSVDecoder {
6161
public init(configuration: Configuration = .init()) {
6262
self.configuration = configuration
6363
}
64+
65+
/// Convenience initializer passing the most used configuration values.
66+
/// - parameter fieldDelimiter: The delimiter between CSV fields.
67+
/// - parameter rowDelimiter: The delimiter between CSV records/rows.
68+
/// - parameter headerStrategy: Whether the CSV data contains headers at the beginning of the file.
69+
public convenience init(fieldDelimiter: Delimiter.Field = .comma, rowDelimiter: Delimiter.Row = .lineFeed, headerStrategy: Strategy.Header = .none) {
70+
self.init(configuration: .init(fieldDelimiter: fieldDelimiter, rowDelimiter: rowDelimiter, headerStrategy: headerStrategy))
71+
}
6472

6573
/// Returns a value of the type you specify decoded from a CSV file.
6674
/// - parameter type: The type of the value to decode from the supplied file.

Tests/CodableCSVTests/CodableTests/DecodingSingleValueTests.swift

Lines changed: 0 additions & 139 deletions
This file was deleted.
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import XCTest
2+
@testable import CodableCSV
3+
4+
/// Tests for empty or single value CSVs.
5+
final class DecodingSinglesTests: XCTestCase {
6+
// List of all tests to run through SPM.
7+
static let allTests = [
8+
("testEmptyFile", testEmptyFile),
9+
("testSingleValueFileWithUnkeyedContainer", testSingleValueFileWithUnkeyedContainer),
10+
("testSingleValueFileWithKeyedContainer", testSingleValueFileWithKeyedContainer),
11+
("testSingleValueFileWithValueContainer", testSingleValueFileWithValueContainer),
12+
("testSingleRowFile", testSingleRowFile),
13+
("testSingleValueRowsWithkeyedContainer", testSingleValueRowsWithkeyedContainer)
14+
]
15+
16+
override func setUp() {
17+
self.continueAfterFailure = false
18+
}
19+
}
20+
21+
extension DecodingSinglesTests {
22+
/// Tests the decoding of a completely empty file.
23+
func testEmptyFile() throws {
24+
let decoder = CSVDecoder(headerStrategy: .none)
25+
26+
struct Custom: Decodable {
27+
init(from decoder: Decoder) throws {
28+
let unkeyedContainer = try decoder.unkeyedContainer()
29+
XCTAssertTrue(unkeyedContainer.isAtEnd)
30+
let keyedContainer = try decoder.container(keyedBy: DecodingKey.self)
31+
XCTAssertFalse(keyedContainer.contains(DecodingKey(0)))
32+
let _ = try decoder.singleValueContainer()
33+
}
34+
}
35+
36+
let data = "".data(using: .utf8)!
37+
let _ = try decoder.decode(Custom.self, from: data, encoding: .utf8)
38+
}
39+
}
40+
41+
extension DecodingSinglesTests {
42+
/// Tests the decoding of file containing a single row with only one value.
43+
///
44+
/// The custom decoding process will request an unkeyed container.
45+
func testSingleValueFileWithUnkeyedContainer() throws {
46+
let rowDelimiter = Delimiter.Row.lineFeed
47+
let decoder = CSVDecoder(rowDelimiter: rowDelimiter, headerStrategy: .none)
48+
49+
struct Custom: Decodable {
50+
let value: String
51+
52+
init(from decoder: Decoder) throws {
53+
var fileContainer = try decoder.unkeyedContainer()
54+
self.value = try fileContainer.decode(String.self)
55+
XCTAssertTrue(fileContainer.isAtEnd)
56+
}
57+
}
58+
59+
let data = ("Grumpy" + rowDelimiter.stringValue!).data(using: .utf8)!
60+
let _ = try decoder.decode(Custom.self, from: data, encoding: .utf8)
61+
}
62+
63+
/// Tests the decoding of file containing a single row with only one value.
64+
///
65+
/// The custom decoding process will request an keyed container.
66+
func testSingleValueFileWithKeyedContainer() throws {
67+
let rowDelimiter = Delimiter.Row.lineFeed
68+
let decoder = CSVDecoder(rowDelimiter: rowDelimiter, headerStrategy: .none)
69+
70+
struct Custom: Decodable {
71+
let value: Int32
72+
73+
init(from decoder: Decoder) throws {
74+
let fileContainer = try decoder.container(keyedBy: CodingKeys.self)
75+
self.value = try fileContainer.decode(Int32.self, forKey: .value)
76+
}
77+
78+
private enum CodingKeys: Int, CodingKey {
79+
case value = 0
80+
}
81+
}
82+
83+
let data = ("34" + rowDelimiter.stringValue!).data(using: .utf8)!
84+
let _ = try decoder.decode(Custom.self, from: data, encoding: .utf8)
85+
}
86+
87+
/// Tests the decoding of file containing a single row with only one value.
88+
///
89+
/// The custom decoding process will request a single value container.
90+
func testSingleValueFileWithValueContainer() throws {
91+
let rowDelimiter = Delimiter.Row.lineFeed
92+
let decoder = CSVDecoder(rowDelimiter: rowDelimiter, headerStrategy: .none)
93+
94+
struct Custom: Decodable {
95+
let value: UInt32
96+
97+
init(from decoder: Decoder) throws {
98+
let wrapperContainer = try decoder.singleValueContainer()
99+
self.value = try wrapperContainer.decode(UInt32.self)
100+
}
101+
}
102+
103+
let data = ("77" + rowDelimiter.stringValue!).data(using: .utf8)!
104+
let _ = try decoder.decode(Custom.self, from: data, encoding: .utf8)
105+
}
106+
}
107+
108+
extension DecodingSinglesTests {
109+
/// Tests the decoding of a file containing a single row with many values.
110+
///
111+
/// The custom decoding process will request a unkeyed container.
112+
func testSingleRowFile() throws {
113+
let rowDelimiter = Delimiter.Row.lineFeed
114+
let decoder = CSVDecoder(rowDelimiter: rowDelimiter, headerStrategy: .none)
115+
116+
struct Custom: Decodable {
117+
private(set) var values: [Int8] = []
118+
119+
init(from decoder: Decoder) throws {
120+
var fileContainer = try decoder.unkeyedContainer()
121+
while !fileContainer.isAtEnd {
122+
values.append(try fileContainer.decode(Int8.self))
123+
}
124+
}
125+
}
126+
127+
let data = (0...10).map { String($0) }
128+
.joined(separator: rowDelimiter.stringValue!)
129+
.data(using: .utf8)!
130+
let _ = try decoder.decode(Custom.self, from: data, encoding: .utf8)
131+
}
132+
133+
/// Tests the decoding of a file containing a single row with many values.
134+
///
135+
/// The custom decoding process will request unkeyed containers and manual decoding.
136+
func testSingleValueRowsWithkeyedContainer() throws {
137+
let rowDelimiter = Delimiter.Row.lineFeed
138+
let decoder = CSVDecoder(rowDelimiter: rowDelimiter, headerStrategy: .none)
139+
140+
struct Custom: Decodable {
141+
private(set) var values: [Int8] = []
142+
143+
init(from decoder: Decoder) throws {
144+
var fileContainer = try decoder.unkeyedContainer()
145+
while !fileContainer.isAtEnd {
146+
var rowContainer = try fileContainer.nestedUnkeyedContainer()
147+
while !rowContainer.isAtEnd {
148+
values.append(try rowContainer.decode(Int8.self))
149+
}
150+
}
151+
}
152+
}
153+
154+
let data = (0...10).map { String($0) }
155+
.joined(separator: rowDelimiter.stringValue!)
156+
.data(using: .utf8)!
157+
let _ = try decoder.decode(Custom.self, from: data, encoding: .utf8)
158+
}
159+
}

0 commit comments

Comments
 (0)