Skip to content

[4.0] EncodingError/DecodingError bridging fixes and conveniences #10704

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 3 additions & 53 deletions stdlib/public/SDK/Foundation/Codable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,59 +14,9 @@
// Errors
//===----------------------------------------------------------------------===//

// Adding the following extensions to EncodingError and DecodingError allows them to bridge to NSErrors implicitly.

fileprivate let NSCodingPathErrorKey = "NSCodingPath"
fileprivate let NSDebugDescriptionErrorKey = "NSDebugDescription"

extension EncodingError : CustomNSError {
public static var errorDomain: String = NSCocoaErrorDomain

public var errorCode: Int {
switch self {
case .invalidValue(_, _): return CocoaError.coderInvalidValue.rawValue
}
}

public var errorUserInfo: [String : Any] {
let context: Context
switch self {
case .invalidValue(_, let c): context = c
}

return [NSCodingPathErrorKey: context.codingPath,
NSDebugDescriptionErrorKey: context.debugDescription]
}
}

extension DecodingError : CustomNSError {
public static var errorDomain: String = NSCocoaErrorDomain

public var errorCode: Int {
switch self {
case .valueNotFound(_, _): fallthrough
case .keyNotFound(_, _):
return CocoaError._coderValueNotFound.rawValue

case .typeMismatch(_, _): fallthrough
case .dataCorrupted(_):
return CocoaError._coderReadCorrupt.rawValue
}
}

public var errorUserInfo: [String : Any]? {
let context: Context
switch self {
case .typeMismatch(_, let c): context = c
case .valueNotFound(_, let c): context = c
case .keyNotFound(_, let c): context = c
case .dataCorrupted(let c): context = c
}

return [NSCodingPathErrorKey: context.codingPath,
NSDebugDescriptionErrorKey: context.debugDescription]
}
}
// Both of these error types bridge to NSError, and through the entry points they use, no further work is needed to make them localized.
extension EncodingError : LocalizedError {}
extension DecodingError : LocalizedError {}

//===----------------------------------------------------------------------===//
// Error Utilities
Expand Down
14 changes: 12 additions & 2 deletions stdlib/public/SDK/Foundation/JSONEncoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,11 @@ open class JSONEncoder {
}

let writingOptions = JSONSerialization.WritingOptions(rawValue: self.outputFormatting.rawValue)
return try JSONSerialization.data(withJSONObject: topLevel, options: writingOptions)
do {
return try JSONSerialization.data(withJSONObject: topLevel, options: writingOptions)
} catch {
throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Unable to encode the given top-level value to JSON.", underlyingError: error))
}
}
}

Expand Down Expand Up @@ -872,7 +876,13 @@ open class JSONDecoder {
/// - throws: `DecodingError.dataCorrupted` if values requested from the payload are corrupted, or if the given data is not valid JSON.
/// - throws: An error if any value throws an error during decoding.
open func decode<T : Decodable>(_ type: T.Type, from data: Data) throws -> T {
let topLevel = try JSONSerialization.jsonObject(with: data)
let topLevel: Any
do {
topLevel = try JSONSerialization.jsonObject(with: data)
} catch {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "The given data was not valid JSON.", underlyingError: error))
}

let decoder = _JSONDecoder(referencing: topLevel, options: self.options)
return try T(from: decoder)
}
Expand Down
16 changes: 0 additions & 16 deletions stdlib/public/SDK/Foundation/NSError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -850,19 +850,11 @@ extension CocoaError.Code {

@available(OSX, introduced: 10.11) @available(iOS, introduced: 9.0)
public static var coderReadCorrupt: CocoaError.Code {
return _coderReadCorrupt
}

internal static var _coderReadCorrupt: CocoaError.Code {
return CocoaError.Code(rawValue: 4864)
}

@available(OSX, introduced: 10.11) @available(iOS, introduced: 9.0)
public static var coderValueNotFound: CocoaError.Code {
return _coderValueNotFound
}

internal static var _coderValueNotFound: CocoaError.Code {
return CocoaError.Code(rawValue: 4865)
}

Expand Down Expand Up @@ -1301,19 +1293,11 @@ extension CocoaError {

@available(OSX, introduced: 10.11) @available(iOS, introduced: 9.0)
public static var coderReadCorrupt: CocoaError.Code {
return _coderReadCorrupt
}

public static var _coderReadCorrupt: CocoaError.Code {
return CocoaError.Code(rawValue: 4864)
}

@available(OSX, introduced: 10.11) @available(iOS, introduced: 9.0)
public static var coderValueNotFound: CocoaError.Code {
return _coderValueNotFound
}

internal static var _coderValueNotFound: CocoaError.Code {
return CocoaError.Code(rawValue: 4865)
}

Expand Down
13 changes: 11 additions & 2 deletions stdlib/public/SDK/Foundation/PlistEncoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,11 @@ open class PropertyListEncoder {
debugDescription: "Top-level \(Value.self) encoded as date property list fragment."))
}

return try PropertyListSerialization.data(fromPropertyList: topLevel, format: self.outputFormat, options: 0)
do {
return try PropertyListSerialization.data(fromPropertyList: topLevel, format: self.outputFormat, options: 0)
} catch {
throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Unable to encode the given top-level value as a property list", underlyingError: error))
}
}
}

Expand Down Expand Up @@ -640,7 +644,12 @@ open class PropertyListDecoder {
/// - throws: `DecodingError.dataCorrupted` if values requested from the payload are corrupted, or if the given data is not a valid property list.
/// - throws: An error if any value throws an error during decoding.
open func decode<T : Decodable>(_ type: T.Type, from data: Data, format: inout PropertyListSerialization.PropertyListFormat) throws -> T {
let topLevel = try PropertyListSerialization.propertyList(from: data, options: [], format: &format)
let topLevel: Any
do {
topLevel = try PropertyListSerialization.propertyList(from: data, options: [], format: &format)
} catch {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "The given data was not a valid property list.", underlyingError: error))
}
let decoder = _PlistDecoder(referencing: topLevel, options: self.options)
return try T(from: decoder)
}
Expand Down
145 changes: 143 additions & 2 deletions stdlib/public/core/Codable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1740,6 +1740,10 @@ public protocol UnkeyedDecodingContainer {
/// A container that can support the storage and direct encoding of a single
/// non-keyed value.
public protocol SingleValueEncodingContainer {
/// The path of coding keys taken to get to this point in encoding.
/// A `nil` value indicates an unkeyed container.
var codingPath: [CodingKey?] { get }

/// Encodes a null value.
///
/// - throws: `EncodingError.invalidValue` if a null value is invalid in the current context for this format.
Expand Down Expand Up @@ -1854,6 +1858,10 @@ public protocol SingleValueEncodingContainer {

/// A `SingleValueDecodingContainer` is a container which can support the storage and direct decoding of a single non-keyed value.
public protocol SingleValueDecodingContainer {
/// The path of coding keys taken to get to this point in encoding.
/// A `nil` value indicates an unkeyed container.
var codingPath: [CodingKey?] { get }

/// Decodes a null value.
///
/// - returns: Whether the encountered value was null.
Expand Down Expand Up @@ -2026,20 +2034,63 @@ public enum EncodingError : Error {
/// A description of what went wrong, for debugging purposes.
public let debugDescription: String

/// The underlying error which caused this error, if any.
public let underlyingError: Error?

/// Initializes `self` with the given path of `CodingKey`s and a description of what went wrong.
///
/// - parameter codingPath: The path of `CodingKey`s taken to get to the point of the failing encode call.
/// - parameter debugDescription: A description of what went wrong, for debugging purposes.
public init(codingPath: [CodingKey?], debugDescription: String) {
/// - parameter underlyingError: The underlying error which caused this error, if any.
public init(codingPath: [CodingKey?], debugDescription: String, underlyingError: Error? = nil) {
self.codingPath = codingPath
self.debugDescription = debugDescription
self.underlyingError = underlyingError
}
}

/// `.invalidValue` indicates that an `Encoder` or its containers could not encode the given value.
///
/// Contains the attempted value, along with context for debugging.
case invalidValue(Any, Context)

// MARK: - NSError Bridging

// CustomNSError bridging applies only when the CustomNSError conformance is applied in the same module as the declared error type.
// Since we cannot access CustomNSError (which is defined in Foundation) from here, we can use the "hidden" entry points.

public var _domain: String {
return "NSCocoaErrorDomain"
}

public var _code: Int {
switch self {
case .invalidValue(_, _): return 4866
}
}

public var _userInfo: AnyObject? {
// The error dictionary must be returned as an AnyObject. We can do this only on platforms with bridging, unfortunately.
#if os(OSX) || os(iOS) || os(watchOS) || os(tvOS)
let context: Context
switch self {
case .invalidValue(_, let c): context = c
}

var userInfo: [String : Any] = [
"NSCodingPath": context.codingPath,
"NSDebugDescription": context.debugDescription
]

if let underlyingError = context.underlyingError {
userInfo["NSUnderlyingError"] = underlyingError
}

return userInfo as AnyObject
#else
return nil
#endif
}
}

/// An error that occurs during the decoding of a value.
Expand All @@ -2052,13 +2103,18 @@ public enum DecodingError : Error {
/// A description of what went wrong, for debugging purposes.
public let debugDescription: String

/// The underlying error which caused this error, if any.
public let underlyingError: Error?

/// Initializes `self` with the given path of `CodingKey`s and a description of what went wrong.
///
/// - parameter codingPath: The path of `CodingKey`s taken to get to the point of the failing decode call.
/// - parameter debugDescription: A description of what went wrong, for debugging purposes.
public init(codingPath: [CodingKey?], debugDescription: String) {
/// - parameter underlyingError: The underlying error which caused this error, if any.
public init(codingPath: [CodingKey?], debugDescription: String, underlyingError: Error? = nil) {
self.codingPath = codingPath
self.debugDescription = debugDescription
self.underlyingError = underlyingError
}
}

Expand All @@ -2081,6 +2137,91 @@ public enum DecodingError : Error {
///
/// Contains context for debugging.
case dataCorrupted(Context)

// MARK: - NSError Bridging

// CustomNSError bridging applies only when the CustomNSError conformance is applied in the same module as the declared error type.
// Since we cannot access CustomNSError (which is defined in Foundation) from here, we can use the "hidden" entry points.

public var _domain: String {
return "NSCocoaErrorDomain"
}

public var _code: Int {
switch self {
case .keyNotFound(_, _): fallthrough
case .valueNotFound(_, _): return 4865
case .typeMismatch(_, _): fallthrough
case .dataCorrupted(_): return 4864
}
}

public var _userInfo: AnyObject? {
// The error dictionary must be returned as an AnyObject. We can do this only on platforms with bridging, unfortunately.
#if os(OSX) || os(iOS) || os(watchOS) || os(tvOS)
let context: Context
switch self {
case .keyNotFound(_, let c): context = c
case .valueNotFound(_, let c): context = c
case .typeMismatch(_, let c): context = c
case .dataCorrupted( let c): context = c
}

var userInfo: [String : Any] = [
"NSCodingPath": context.codingPath,
"NSDebugDescription": context.debugDescription
]

if let underlyingError = context.underlyingError {
userInfo["NSUnderlyingError"] = underlyingError
}

return userInfo as AnyObject
#else
return nil
#endif
}
}

// The following extensions allow for easier error construction.

public extension DecodingError {
/// A convenience method which creates a new .dataCorrupted error using a constructed coding path and the given debug description.
///
/// Constructs a coding path by appending the given key to the given container's coding path.
///
/// - param key: The key which caused the failure.
/// - param container: The container in which the corrupted data was accessed.
/// - param debugDescription: A description of the error to aid in debugging.
static func dataCorruptedError<C : KeyedDecodingContainerProtocol>(forKey key: C.Key, in container: C, debugDescription: String) -> DecodingError {
let context = DecodingError.Context(codingPath: container.codingPath + [key],
debugDescription: debugDescription)
return .dataCorrupted(context)
}

/// A convenience method which creates a new .dataCorrupted error using a constructed coding path and the given debug description.
///
/// Constructs a coding path by appending a nil key to the given container's coding path.
///
/// - param container: The container in which the corrupted data was accessed.
/// - param debugDescription: A description of the error to aid in debugging.
static func dataCorruptedError(in container: UnkeyedDecodingContainer, debugDescription: String) -> DecodingError {
let context = DecodingError.Context(codingPath: container.codingPath + [nil],
debugDescription: debugDescription)
return .dataCorrupted(context)
}

/// A convenience method which creates a new .dataCorrupted error using a constructed coding path and the given debug description.
///
/// Uses the given container's coding path as the constructed path.
///
/// - param container: The container in which the corrupted data was accessed.
/// - param debugDescription: A description of the error to aid in debugging.
static func dataCorruptedError(in container: SingleValueDecodingContainer, debugDescription: String) -> DecodingError {
let context = DecodingError.Context(codingPath: container.codingPath,
debugDescription: debugDescription)
return .dataCorrupted(context)
}
}

//===----------------------------------------------------------------------===//
Expand Down