Add userInfo protocols to standard library

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.