Because Combine’s TopLevelDecoder
doesn’t require userInfo
, and no protocol exists that only requires userInfo
, it is more or less impossible to write a generic method that interacts with the userInfo
dictionary of a type conforming to TopLevelDecoder
. This is in spite of the fact that Decoder
requires it, so every TopLevelDecoder
must at some point supply such a dictionary.
I ran into this problem while trying to write a publisher that operated on an NSManagedObjectContext
stored in a TopLevelDecoder
.
import Combine
import class CoreData.NSManagedObjectContext
extension Publisher {
/// Decodes the output from upstream using a specified decoder.
///
/// If `decoder` has an `NSManagedObjectContext` inside a `userInfo` dictionary with a `CodingUserInfoKey` of `context`, decoding will be performed on that context's private queue.
func performDecode<Item: Decodable, Coder: TopLevelDecoder>(type: Item.Type, decoder: Coder)
-> Publishers.FlatMap<Future<Item, Error>, Publishers.MapError<Self, Error>>
where Self.Output == Coder.Input {
mapError { $0 as Error }.flatMap { (element: Coder.Input) in
Future { promise in
let decode = { promise(.init { try decoder.decode(type, from: element) }) }
guard
let context = Mirror(reflecting: decoder).children.first(where: { $0.label == "userInfo" }
).map({ $0.value as? [CodingUserInfoKey: Any] })?.map({ userInfo in
CodingUserInfoKey(rawValue: "context").flatMap { userInfo[$0] }
}) as? NSManagedObjectContext
else { return decode() }
return context.perform(decode)
}
}
}
}
Needless to say, this is not ideal. My initial solution was to ask that userInfo
be required by TopLevelDecoder
, since it is required by Decoder
. However, I’ve since concluded that the Combine team had the right idea when they excluded it: why require something you don’t need?
Let’s see how I could write this if the protocols I am proposing existed:
import Combine
import class CoreData.NSManagedObjectContext
extension Publisher {
/// Decodes the output from upstream using a specified decoder.
///
/// If `decoder` has an `NSManagedObjectContext` inside its `userInfo` dictionary with a key of `context`, decoding will be performed on that context's private queue.
func performDecode<Item: Decodable, Coder: TopLevelDecoder & UserInfoProviding>(
type: Item.Type, decoder: Coder
) -> Publishers.FlatMap<Future<Item, Error>, Publishers.MapError<Self, Error>>
where
Self.Output == Coder.Input, Coder.UserInfoKey: RawRepresentable,
Coder.UserInfoKey.RawValue: ExpressibleByStringLiteral
{
mapError { $0 as Error }.flatMap { (element: Coder.Input) in
Future { promise in
let decode = { promise(.init { try decoder.decode(type, from: element) }) }
if let context = Coder.UserInfoKey(rawValue: "context").flatMap({
decoder.userInfo[$0] as? NSManagedObjectContext
}) {
return context.perform(decode)
} else {
return decode()
}
}
}
}
}
That is much more resilient, and allows callees as much flexibility as possible. I don’t think I can provide a more concrete example than that.