Skip to content

Commit 5f7e96e

Browse files
committed
Extracted DecodingErrors
1 parent 04942a2 commit 5f7e96e

File tree

6 files changed

+131
-60
lines changed

6 files changed

+131
-60
lines changed

sources/Codable/Decodable/Containers/DecodingKeyed.swift

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ extension ShadowDecoder {
1818
init(decoder: ShadowDecoder) throws {
1919
switch decoder.codingPath.count {
2020
case 0: self.focus = .file
21-
case 1: let r = try decoder.codingPath[0].intValue ?! DecodingError.invalidRowKey(codingPath: decoder.codingPath)
21+
case 1: let key = decoder.codingPath[0]
22+
let r = try key.intValue ?! DecodingError.invalidKey(forRow: key, codingPath: decoder.codingPath)
2223
self.focus = .row(r)
2324
default: throw DecodingError.invalidContainerRequest(codingPath: decoder.codingPath)
2425
}
@@ -71,7 +72,7 @@ extension ShadowDecoder.KeyedContainer {
7172
func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey {
7273
switch self.focus {
7374
case .file:
74-
guard let rowIndex = key.intValue else { throw DecodingError.invalidRowKey(codingPath: self.codingPath + [key]) }
75+
guard let rowIndex = key.intValue else { throw DecodingError.invalidKey(forRow: key, codingPath: self.codingPath + [key]) }
7576
var codingPath = self.decoder.codingPath; codingPath.append(DecodingKey(rowIndex))
7677
let decoder = ShadowDecoder(source: self.decoder.source, codingPath: codingPath)
7778
return KeyedDecodingContainer(ShadowDecoder.KeyedContainer<NestedKey>(unsafeDecoder: decoder, rowIndex: rowIndex))
@@ -82,7 +83,7 @@ extension ShadowDecoder.KeyedContainer {
8283
func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer {
8384
switch self.focus {
8485
case .file:
85-
guard let rowIndex = key.intValue else { throw DecodingError.invalidRowKey(codingPath: self.codingPath + [key]) }
86+
guard let rowIndex = key.intValue else { throw DecodingError.invalidKey(forRow: key, codingPath: self.codingPath + [key]) }
8687
var codingPath = self.decoder.codingPath; codingPath.append(DecodingKey(rowIndex))
8788
let decoder = ShadowDecoder(source: self.decoder.source, codingPath: codingPath)
8889
return ShadowDecoder.UnkeyedContainer(unsafeDecoder: decoder, rowIndex: rowIndex)
@@ -93,7 +94,7 @@ extension ShadowDecoder.KeyedContainer {
9394
func superDecoder(forKey key: Key) throws -> Decoder {
9495
switch self.focus {
9596
case .file:
96-
guard let rowIndex = key.intValue else { throw DecodingError.invalidRowKey(codingPath: self.codingPath + [key]) }
97+
guard let rowIndex = key.intValue else { throw DecodingError.invalidKey(forRow: key, codingPath: self.codingPath + [key]) }
9798
var codingPath = self.decoder.codingPath; codingPath.append(DecodingKey(rowIndex))
9899
return ShadowDecoder(source: self.decoder.source, codingPath: codingPath)
99100
case .row: throw DecodingError.invalidContainerRequest(codingPath: self.codingPath)
@@ -272,7 +273,7 @@ extension ShadowDecoder.KeyedContainer {
272273
decoder = ShadowDecoder(source: self.decoder.source, codingPath: codingPath)
273274
case .file:
274275
guard let rowIndex = key.intValue else {
275-
throw DecodingError.invalidRowKey(codingPath: self.codingPath + [key])
276+
throw DecodingError.invalidKey(forRow: key, codingPath: self.codingPath + [key])
276277
}
277278
// Values are only allowed to be decoded directly from a nested container in "file level" if the CSV rows have a single column.
278279
guard self.decoder.source.numFields == 1 else {
@@ -289,3 +290,27 @@ extension ShadowDecoder.KeyedContainer {
289290
return .init(unsafeDecoder: decoder, rowIndex: index.row, fieldIndex: index.field)
290291
}
291292
}
293+
294+
fileprivate extension DecodingError {
295+
/// Error raised when a coding key representing a row within the CSV file cannot be transformed into an integer value.
296+
/// - parameter codingPath: The whole coding path, including the invalid row key.
297+
static func invalidKey(forRow key: CodingKey, codingPath: [CodingKey]) -> DecodingError {
298+
DecodingError.keyNotFound(key, .init(
299+
codingPath: codingPath,
300+
debugDescription: "The coding key identifying a CSV row couldn't be transformed into an integer value."))
301+
}
302+
/// Error raised when a single value container is requested on an invalid coding path.
303+
/// - parameter codingPath: The full chain of containers which generated this error.
304+
static func invalidContainerRequest(codingPath: [CodingKey]) -> DecodingError {
305+
DecodingError.dataCorrupted(
306+
Context(codingPath: codingPath,
307+
debugDescription: "CSV doesn't support more than two nested decoding container.")
308+
)
309+
}
310+
/// Error raised when a value is decoded, but a container was expected by the decoder.
311+
static func invalidNestedRequired(codingPath: [CodingKey]) -> DecodingError {
312+
DecodingError.dataCorrupted(.init(
313+
codingPath: codingPath,
314+
debugDescription: "A nested container is needed to decode CSV row values"))
315+
}
316+
}

sources/Codable/Decodable/Containers/DecodingUnkeyed.swift

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ extension ShadowDecoder {
1919
init(decoder: ShadowDecoder) throws {
2020
switch decoder.codingPath.count {
2121
case 0: self.focus = .file
22-
case 1: let r = try decoder.codingPath[0].intValue ?! DecodingError.invalidRowKey(codingPath: decoder.codingPath)
22+
case 1: let key = decoder.codingPath[0]
23+
let r = try key.intValue ?! DecodingError.invalidKey(forRow: key, codingPath: decoder.codingPath)
2324
self.focus = .row(r)
2425
default: throw DecodingError.invalidContainerRequest(codingPath: decoder.codingPath)
2526
}
@@ -301,3 +302,27 @@ extension ShadowDecoder.UnkeyedContainer {
301302
return .init(unsafeDecoder: decoder, rowIndex: index.row, fieldIndex: index.field)
302303
}
303304
}
305+
306+
fileprivate extension DecodingError {
307+
/// Error raised when a coding key representing a row within the CSV file cannot be transformed into an integer value.
308+
/// - parameter codingPath: The whole coding path, including the invalid row key.
309+
static func invalidKey(forRow key: CodingKey, codingPath: [CodingKey]) -> DecodingError {
310+
DecodingError.keyNotFound(key, .init(
311+
codingPath: codingPath,
312+
debugDescription: "The coding key identifying a CSV row couldn't be transformed into an integer value."))
313+
}
314+
/// Error raised when a single value container is requested on an invalid coding path.
315+
/// - parameter codingPath: The full chain of containers which generated this error.
316+
static func invalidContainerRequest(codingPath: [CodingKey]) -> DecodingError {
317+
DecodingError.dataCorrupted(
318+
Context(codingPath: codingPath,
319+
debugDescription: "CSV doesn't support more than two nested decoding container.")
320+
)
321+
}
322+
/// Error raised when a value is decoded, but a container was expected by the decoder.
323+
static func invalidNestedRequired(codingPath: [CodingKey]) -> DecodingError {
324+
DecodingError.dataCorrupted(.init(
325+
codingPath: codingPath,
326+
debugDescription: "A nested container is needed to decode CSV row values"))
327+
}
328+
}

sources/Codable/Decodable/Containers/DecodingValue.swift

Lines changed: 73 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@ extension ShadowDecoder {
1818
init(decoder: ShadowDecoder) throws {
1919
switch decoder.codingPath.count {
2020
case 2: let key = (row: decoder.codingPath[0], field: decoder.codingPath[1])
21-
let r = try key.row.intValue ?! DecodingError.invalidRowKey(codingPath: decoder.codingPath)
21+
let r = try key.row.intValue ?! DecodingError.invalidKey(forRow: key.row, codingPath: decoder.codingPath)
2222
let f = try decoder.source.fieldIndex(forKey: key.field, codingPath: decoder.codingPath)
2323
self.focus = .field(r, f)
24-
case 1: let r = try decoder.codingPath[0].intValue ?! DecodingError.invalidRowKey(codingPath: decoder.codingPath)
24+
case 1: let key = decoder.codingPath[0]
25+
let r = try key.intValue ?! DecodingError.invalidKey(forRow: key, codingPath: decoder.codingPath)
2526
self.focus = .row(r)
2627
case 0: self.focus = .file
2728
default: throw DecodingError.invalidContainerRequest(codingPath: decoder.codingPath)
@@ -165,19 +166,19 @@ extension ShadowDecoder.SingleValueContainer {
165166
private func lowlevelDecode<T>(transform: (String) -> T?) throws -> T {
166167
switch self.focus {
167168
case .field(let rowIndex, let fieldIndex):
168-
let value = try self.decoder.source.field(at: rowIndex, fieldIndex)
169-
return try transform(value) ?! DecodingError.typeMismatch(T.self, .invalidTransformation(value: value, codingPath: self.codingPath))
169+
let string = try self.decoder.source.field(at: rowIndex, fieldIndex)
170+
return try transform(string) ?! DecodingError.invalid(type: T.self, string: string, codingPath: self.codingPath)
170171
case .row(let rowIndex):
171172
// Values are only allowed to be decoded directly from a single value container in "row level" if the CSV rows have a single column.
172173
guard self.decoder.source.numFields == 1 else { throw DecodingError.invalidNestedRequired(codingPath: self.codingPath) }
173-
let value = try self.decoder.source.field(at: rowIndex, 0)
174-
return try transform(value) ?! DecodingError.typeMismatch(T.self, .invalidTransformation(value: value, codingPath: self.codingPath + [DecodingKey(0)]))
174+
let string = try self.decoder.source.field(at: rowIndex, 0)
175+
return try transform(string) ?! DecodingError.invalid(type: T.self, string: string, codingPath: self.codingPath + [DecodingKey(0)])
175176
case .file:
176177
let source = self.decoder.source
177178
// 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.
178179
if source.isRowAtEnd(index: 1), source.numFields == 1 {
179-
let value = try self.decoder.source.field(at: 0, 0)
180-
return try transform(value) ?! DecodingError.typeMismatch(T.self, .invalidTransformation(value: value, codingPath: self.codingPath + [DecodingKey(0), DecodingKey(0)]))
180+
let string = try self.decoder.source.field(at: 0, 0)
181+
return try transform(string) ?! DecodingError.invalid(type: T.self, string: string, codingPath: self.codingPath + [DecodingKey(0), DecodingKey(0)])
181182
} else {
182183
throw DecodingError.invalidNestedRequired(codingPath: self.codingPath)
183184
}
@@ -199,14 +200,10 @@ extension ShadowDecoder.SingleValueContainer {
199200
return Foundation.Date(timeIntervalSince1970: number / 1000.0)
200201
case .iso8601:
201202
let string = try self.decode(String.self)
202-
return try DateFormatter.iso8601.date(from: string) ?! DecodingError.dataCorrupted(.init(
203-
codingPath: self.codingPath,
204-
debugDescription: "The string '\(string)' couldn't be transformed into a Date using the '.iso8601' strategy."))
203+
return try DateFormatter.iso8601.date(from: string) ?! DecodingError.invalidDateISO(string: string, codingPath: self.codingPath)
205204
case .formatted(let formatter):
206205
let string = try self.decode(String.self)
207-
return try formatter.date(from: string) ?! DecodingError.dataCorrupted(.init(
208-
codingPath: self.codingPath,
209-
debugDescription: "The string '\(string)' couldn't be transformed into a Date using the '.formatted' strategy."))
206+
return try formatter.date(from: string) ?! DecodingError.invalidDateFormatted(string: string, codingPath: self.codingPath)
210207
case .custom(let closure):
211208
return try closure(self.decoder)
212209
}
@@ -221,7 +218,7 @@ extension ShadowDecoder.SingleValueContainer {
221218
return try Data(from: self.decoder)
222219
case .base64:
223220
let string = try self.decode(String.self)
224-
return try Data(base64Encoded: string) ?! DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "The following string is not valid Base64:\n'\(string)'"))
221+
return try Data(base64Encoded: string) ?! DecodingError.invalidData64(string: string, codingPath: self.codingPath)
225222
case .custom(let closure):
226223
return try closure(self.decoder)
227224
}
@@ -234,9 +231,7 @@ extension ShadowDecoder.SingleValueContainer {
234231
switch self.decoder.source.configuration.decimalStrategy {
235232
case .locale(let locale):
236233
let string = try self.decode(String.self)
237-
return try Decimal(string: string, locale: locale) ?! DecodingError.dataCorrupted(.init(
238-
codingPath: self.codingPath,
239-
debugDescription: "The string '\(string)' couldn't be transformed into a Decimal using the '.locale' strategy"))
234+
return try Decimal(string: string, locale: locale) ?! DecodingError.invalidDecimal(string: string, locale: locale, codingPath: self.codingPath)
240235
case .custom(let closure):
241236
return try closure(self.decoder)
242237
}
@@ -249,3 +244,63 @@ extension ShadowDecoder.SingleValueContainer {
249244
try self.lowlevelDecode { URL(string: $0) }
250245
}
251246
}
247+
248+
fileprivate extension DecodingError {
249+
/// Error raised when a coding key representing a row within the CSV file cannot be transformed into an integer value.
250+
/// - parameter codingPath: The whole coding path, including the invalid row key.
251+
static func invalidKey(forRow key: CodingKey, codingPath: [CodingKey]) -> DecodingError {
252+
DecodingError.keyNotFound(key, .init(
253+
codingPath: codingPath,
254+
debugDescription: "The coding key identifying a CSV row couldn't be transformed into an integer value."))
255+
}
256+
/// Error raised when a single value container is requested on an invalid coding path.
257+
/// - parameter codingPath: The full chain of containers which generated this error.
258+
static func invalidContainerRequest(codingPath: [CodingKey]) -> DecodingError {
259+
DecodingError.dataCorrupted(
260+
Context(codingPath: codingPath,
261+
debugDescription: "CSV doesn't support more than two nested decoding container.")
262+
)
263+
}
264+
/// Error raised when a value is decoded, but a container was expected by the decoder.
265+
static func invalidNestedRequired(codingPath: [CodingKey]) -> DecodingError {
266+
DecodingError.dataCorrupted(.init(
267+
codingPath: codingPath,
268+
debugDescription: "A nested container is needed to decode CSV row values"))
269+
}
270+
/// Error raised when transforming a `String` value into another type.
271+
/// - parameter value: The `String` value, which couldn't be transformed.
272+
/// - parameter codingPath: The full chain of containers when this error was generated.
273+
static func invalid<T>(type: T.Type, string: String, codingPath: [CodingKey]) -> DecodingError {
274+
DecodingError.typeMismatch(type,
275+
Context(codingPath: codingPath,
276+
debugDescription: "The field '\(string)' was not of the expected type '\(type)'.")
277+
)
278+
}
279+
/// Error raised when a string value cannot be transformed into a `Date` using the ISO 8601 format.
280+
static func invalidDateISO(string: String, codingPath: [CodingKey]) -> DecodingError {
281+
DecodingError.dataCorrupted(
282+
Context(codingPath: codingPath,
283+
debugDescription: "The field '\(string)' couldn't be transformed into a Date using the '.iso8601' strategy.")
284+
)
285+
}
286+
/// Error raised when a string value cannot be transformed into a `Date` using the ISO 8601 format.
287+
static func invalidDateFormatted(string: String, codingPath: [CodingKey]) -> DecodingError {
288+
DecodingError.dataCorrupted(
289+
Context(codingPath: codingPath,
290+
debugDescription: "The field '\(string)' couldn't be transformed into a Date using the '.formatted' strategy.")
291+
)
292+
}
293+
/// Error raised when a string value cannot be transformed into a Base64 data blob.
294+
static func invalidData64(string: String, codingPath: [CodingKey]) -> DecodingError {
295+
DecodingError.dataCorrupted(
296+
Context(codingPath: codingPath,
297+
debugDescription: "The field '\(string)' couldn't be transformed into a Base64 data blob.")
298+
)
299+
}
300+
/// Error raised when a string value cannot be transformed into a decimal number.
301+
static func invalidDecimal(string: String, locale: Locale?, codingPath: [CodingKey]) -> DecodingError {
302+
var description = "The string '\(string)' couldn't be transformed into a Decimal using the '.locale' strategy."
303+
if let l = locale { description.append(" with locale '\(l)'") }
304+
return DecodingError.dataCorrupted(Context(codingPath: codingPath, debugDescription: description))
305+
}
306+
}

sources/Codable/Decodable/DecodingKey.swift

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -25,37 +25,3 @@ internal struct DecodingKey: CodingKey {
2525
self.index
2626
}
2727
}
28-
29-
internal extension DecodingError {
30-
/// Error occurring when a nested container is requested on an invalid coding path.
31-
/// - parameter codingPath: The full chain of containers which generated this error.
32-
static func invalidContainerRequest(codingPath: [CodingKey]) -> DecodingError {
33-
DecodingError.dataCorrupted(.init(
34-
codingPath: codingPath,
35-
debugDescription: "A container cannot be requested matching the coding path"))
36-
}
37-
38-
/// Error occurring when a coding key representing a row within the CSV file cannot be transformed into an integer value.
39-
/// - parameter codingPath: The whole coding path, including the invalid row key.
40-
static func invalidRowKey(codingPath: [CodingKey]) -> DecodingError {
41-
DecodingError.keyNotFound(codingPath.last!, .init(
42-
codingPath: codingPath.dropLast(),
43-
debugDescription: "The provided coding key identifying a row couldn't be transformed into an integer value"))
44-
}
45-
46-
/// Error occurring when a value is decoded, but a container was expected by the decoder.
47-
static func invalidNestedRequired(codingPath: [CodingKey]) -> DecodingError {
48-
DecodingError.dataCorrupted(.init(
49-
codingPath: codingPath,
50-
debugDescription: "A nested container is needed to decode CSV row values"))
51-
}
52-
}
53-
54-
internal extension DecodingError.Context {
55-
/// Error occurring when transforming a `String` value into another type.
56-
/// - parameter value: The `String` value, which couldn't be transformed.
57-
/// - parameter codingPath: The full chain of containers when this error was generated.
58-
static func invalidTransformation(value: String, codingPath: [CodingKey]) -> DecodingError.Context {
59-
.init(codingPath: codingPath, debugDescription: "The CSV field '\(value)' was not of the expected type.")
60-
}
61-
}

sources/Codable/Encodable/Encoder.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Foundation
22

33
/// Instances of this class are capable of encoding types into CSV files.
4-
@dynamicMemberLookup internal class CSVEncoder {
4+
@dynamicMemberLookup public class CSVEncoder {
55
/// Wrap all configurations in a single easy-to-use structure.
66
private final var configuration: Configuration
77
/// A dictionary you use to customize the encoding process by providing contextual information.

sources/Codable/Encodable/EncoderConfiguration.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ extension CSVEncoder.Configuration {
2424
/// Gives direct access to all CSV writer's configuration values.
2525
/// - parameter member: Writable key path for the writer's configuration values.
2626
public subscript<V>(dynamicMember member: WritableKeyPath<CSVWriter.Configuration,V>) -> V {
27-
get { self.writerConfiguration[keyPath: member] }
27+
@inlinable get { self.writerConfiguration[keyPath: member] }
2828
set { self.writerConfiguration[keyPath: member] = newValue }
2929
}
3030
}

0 commit comments

Comments
 (0)