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.