blob: d4ca6669821cb061491aca780dbbe07b7754bcee [file] [log] [blame]
import Foundation
/// CodableVariable wraps a `Codable` instance to make it compatible with @objc methods.
///
/// `Codable` documentation: https://developer.apple.com/documentation/swift/codable
///
/// The Swift compiler doesn't allow developers to declare an @objc method for eDO as below:
///
/// ```
/// @objc public FooClass : NSObject {
/// @objc public func callBar(value: Int?) // Compile Error!
/// }
/// ```
///
/// The same issue also applies to other auto-synthesized Codable types, including classes, structs
/// and enums (Swift 5.5+). This is because Optional<Int> is a pure Swift type that cannot be
/// represented in Objective-C land. As a workaround, developers can declare a new method in the
/// class extension as below:
///
/// ```
/// extension FooClass {
/// @objc public func callBar(codedValue: CodableVariable) throws {
/// self.callBar(value: try codedValue.unwrap(Int?.self))
/// }
/// }
/// ```
///
/// The new method is compatible with @objc and forwards the coded variables to the original method.
/// So developers can use the new method in the remote call:
///
/// ```
/// var value : Int?
/// // Do something...
/// try remoteFooInstance.callBar(codedValue: try CodableVariable.wrap(value))
/// ```
@objc(EDOCodableVariable)
public class CodableVariable: NSObject, NSSecureCoding, Codable {
/// Error thrown by `CodableVariable` during the decoding.
public enum DecodingError: Error, LocalizedError {
/// The type of encoded data doesn't match the caller's expecting type.
/// - expectedType: The type expected by the decoding method caller.
/// - actualType: The type of the encoded data.
case typeUnmatched(expectedType: String, actualType: String)
public var errorDescription: String? {
switch self {
case let .typeUnmatched(expectedType, actualType):
return "Expecting to decode \(expectedType) but the codable variable is \(actualType)."
}
}
}
internal static let typeKey = "EDOTypeKey"
internal let data: Data
internal let type: String
/// Creates `CodableVariable` instance with a `Codable` instance.
///
/// - Parameter parameter: The Codable instance to be wrapped.
/// - Returns: A `CodableVariable` instance.
/// - Throws: Errors propagated from JSONEncoder when encoding `parameter`.
public static func wrap<T: Encodable>(_ parameter: T) throws -> CodableVariable {
let encoder = JSONEncoder()
return CodableVariable(data: try encoder.encode(parameter), type: String(describing: T.self))
}
internal init(data: Data, type: String) {
self.data = data
self.type = type
}
/// Decodes the Codable instance.
///
/// - Parameter type: The expected type of the decoded instance.
/// - Returns: The decoded instance of `type`.
/// - Throws: `CodableVariable.DecodingError` if decoding fails.
public func unwrap<T: Decodable>() throws -> T {
guard self.type == String(describing: T.self) else {
throw DecodingError.typeUnmatched(
expectedType: self.type,
actualType: String(describing: T.self))
}
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
}
// MARK - NSSecureCoding
public required init?(coder: NSCoder) {
guard let data = coder.decodeData(),
let type = coder.decodeObject(forKey: CodableVariable.typeKey) as? String
else {
return nil
}
self.data = data
self.type = type
}
public func encode(with coder: NSCoder) {
coder.encode(data)
coder.encode(type, forKey: CodableVariable.typeKey)
}
@objc public override var edo_isEDOValueType: Bool { return true }
@objc public static var supportsSecureCoding: Bool { return true }
}
/// Extends Encodable to easily produce `CodableVariable` from the instance.
extension Encodable {
/// Produces a `CodableVariable` instance from `self`.
public var eDOCodableVariable: CodableVariable {
// try! is used here because`CodableVariable.wrap` only throws programmer errors when
// JSONEncoder fails to encode a `Encodable` type.
return try! CodableVariable.wrap(self)
}
}
extension Array where Element: AnyObject {
// Workaround to interact with a Swift array obtained via eDO to avoid the type mismatch failure
// when bridging between NSArray and Swift Array.
public var localArray: Self {
(self as [AnyObject]).map { unsafeBitCast($0, to: Element.self) }
}
}