You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
session-ios/SessionUtilitiesKit/Networking/PreparedRequest.swift

729 lines
33 KiB
Swift

// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Combine
import GRDB
// MARK: - Network.PreparedRequest<R>
public extension Network {
struct PreparedRequest<R> {
public struct CachedResponse {
fileprivate let info: ResponseInfoType
fileprivate let originalData: Any
fileprivate let convertedData: R
}
public let request: URLRequest
public let target: any RequestTarget
public let originalType: Decodable.Type
public let responseType: R.Type
public let retryCount: Int
public let timeout: TimeInterval
public let cachedResponse: CachedResponse?
fileprivate let responseConverter: ((ResponseInfoType, Any) throws -> R)
public let subscriptionHandler: (() -> Void)?
public let outputEventHandler: (((CachedResponse)) -> Void)?
public let completionEventHandler: ((Subscribers.Completion<Error>) -> Void)?
public let cancelEventHandler: (() -> Void)?
// The following types are needed for `BatchRequest` handling
public let method: HTTPMethod
private let path: String
public let endpoint: (any EndpointType)
public let endpointName: String
public let batchEndpoints: [any EndpointType]
public let batchRequestVariant: Network.BatchRequest.Child.Variant
public let batchResponseTypes: [Decodable.Type]
public let requireAllBatchResponses: Bool
public let excludedSubRequestHeaders: [String]
private let jsonKeyedBodyEncoder: ((inout KeyedEncodingContainer<Network.BatchRequest.Child.CodingKeys>, Network.BatchRequest.Child.CodingKeys) throws -> ())?
private let jsonBodyEncoder: ((inout SingleValueEncodingContainer) throws -> ())?
private let b64: String?
private let bytes: [UInt8]?
public init<T: Encodable, E: EndpointType>(
request: Request<T, E>,
urlRequest: URLRequest,
responseType: R.Type,
requireAllBatchResponses: Bool = true,
retryCount: Int = 0,
timeout: TimeInterval
) where R: Decodable {
let batchRequests: [Network.BatchRequest.Child]? = (request.body as? BatchRequestChildRetrievable)?.requests
let batchEndpoints: [E] = (batchRequests?
.compactMap { $0.request.batchRequestEndpoint(of: E.self) })
.defaulting(to: [])
let batchResponseTypes: [Decodable.Type]? = (batchRequests?
.compactMap { batchRequest -> [Decodable.Type]? in
guard batchRequest.request.batchRequestEndpoint(of: E.self) != nil else { return nil }
return batchRequest.request.batchResponseTypes
}
.flatMap { $0 })
self.request = urlRequest
self.target = request.target
self.originalType = R.self
self.responseType = responseType
self.retryCount = retryCount
self.timeout = timeout
self.cachedResponse = nil
// When we are making a batch request we also want to call though any sub request event
// handlers (this allows a lot more reusability for individual requests to manage their
// own results or custom handling just when triggered via a batch request)
self.responseConverter = {
guard
let subRequestResponseConverters: [(Int, ((ResponseInfoType, Any) throws -> Any))] = batchRequests?
.enumerated()
.compactMap({ ($0.0, $0.1.request.erasedResponseConverter) }),
!subRequestResponseConverters.isEmpty
else {
return { info, response in
guard let validResponse: R = response as? R else { throw NetworkError.invalidResponse }
return validResponse
}
}
// Results are returned in the same order they were made in so we can use the matching
// indexes to get the correct response
return { info, response in
let convertedResponse: Any = try {
switch response {
case let batchResponse as Network.BatchResponse:
return Network.BatchResponse(
data: try subRequestResponseConverters
.map { index, responseConverter in
guard batchResponse.count > index else {
throw NetworkError.invalidResponse
}
return try responseConverter(info, batchResponse[index])
}
)
case let batchResponseMap as Network.BatchResponseMap<E>:
return Network.BatchResponseMap(
data: try subRequestResponseConverters
.reduce(into: [E: Any]()) { result, subResponse in
let index: Int = subResponse.0
let responseConverter: ((ResponseInfoType, Any) throws -> Any) = subResponse.1
guard
batchEndpoints.count > index,
let targetResponse: Any = batchResponseMap[batchEndpoints[index]]
else { throw NetworkError.invalidResponse }
let endpoint: E = batchEndpoints[index]
result[endpoint] = try responseConverter(info, targetResponse)
}
)
default: throw NetworkError.invalidResponse
}
}()
guard let validResponse: R = convertedResponse as? R else {
SNLog("[PreparedRequest] Unable to convert responses for missing response")
throw NetworkError.invalidResponse
}
return validResponse
}
}()
self.outputEventHandler = {
guard
let subRequestEventHandlers: [(Int, ((ResponseInfoType, Any, Any) -> Void))] = batchRequests?
.enumerated()
.compactMap({ index, batchRequest in
batchRequest.request.erasedOutputEventHandler.map { (index, $0) }
}),
!subRequestEventHandlers.isEmpty
else { return nil }
// Results are returned in the same order they were made in so we can use the matching
// indexes to get the correct response
return { data in
switch data.originalData {
case let batchResponse as Network.BatchResponse:
subRequestEventHandlers.forEach { index, eventHandler in
guard batchResponse.count > index else {
SNLog("[PreparedRequest] Unable to handle output events for missing response")
return
}
eventHandler(data.info, batchResponse[index], batchResponse[index])
}
case let batchResponseMap as Network.BatchResponseMap<E>:
subRequestEventHandlers.forEach { index, eventHandler in
guard
batchEndpoints.count > index,
let targetResponse: Any = batchResponseMap[batchEndpoints[index]]
else {
SNLog("[PreparedRequest] Unable to handle output events for missing response")
return
}
eventHandler(data.info, targetResponse, targetResponse)
}
default: SNLog("[PreparedRequest] Unable to handle output events for unknown batch response type")
}
}
}()
self.subscriptionHandler = nil
self.completionEventHandler = {
guard
let subRequestEventHandlers: [((Subscribers.Completion<Error>) -> Void)] = batchRequests?
.compactMap({ $0.request.completionEventHandler }),
!subRequestEventHandlers.isEmpty
else { return nil }
// Since the completion event doesn't provide us with any data we can't return the
// individual subRequest results here
return { result in subRequestEventHandlers.forEach { $0(result) } }
}()
self.cancelEventHandler = {
guard
let subRequestEventHandlers: [(() -> Void)] = batchRequests?
.compactMap({ $0.request.cancelEventHandler }),
!subRequestEventHandlers.isEmpty
else { return nil }
return { subRequestEventHandlers.forEach { $0() } }
}()
// The following data is needed in this type for handling batch requests
self.method = request.method
self.endpoint = request.endpoint
self.endpointName = E.name
self.path = request.target.urlPathAndParamsString
self.batchEndpoints = batchEndpoints
self.batchRequestVariant = E.batchRequestVariant
self.batchResponseTypes = batchResponseTypes.defaulting(to: [Network.BatchSubResponse<R>.self])
self.requireAllBatchResponses = requireAllBatchResponses
self.excludedSubRequestHeaders = E.excludedSubRequestHeaders
if batchRequests != nil && self.batchEndpoints.count != self.batchResponseTypes.count {
SNLog("[PreparedRequest] Created with invalid sub requests")
}
// Note: Need to differentiate between JSON, b64 string and bytes body values to ensure
// they are encoded correctly so the server knows how to handle them
switch request.body {
case let bodyString as String:
self.jsonKeyedBodyEncoder = nil
self.jsonBodyEncoder = nil
self.b64 = bodyString
self.bytes = nil
case let bodyBytes as [UInt8]:
self.jsonKeyedBodyEncoder = nil
self.jsonBodyEncoder = nil
self.b64 = nil
self.bytes = bodyBytes
default:
self.jsonKeyedBodyEncoder = { [body = request.body] container, key in
try container.encodeIfPresent(body, forKey: key)
}
self.jsonBodyEncoder = { [body = request.body] container in
try container.encode(body)
}
self.b64 = nil
self.bytes = nil
}
}
fileprivate init<U: Decodable>(
request: URLRequest,
target: any RequestTarget,
originalType: U.Type,
responseType: R.Type,
retryCount: Int,
timeout: TimeInterval,
cachedResponse: CachedResponse?,
responseConverter: @escaping (ResponseInfoType, Any) throws -> R,
subscriptionHandler: (() -> Void)?,
outputEventHandler: ((CachedResponse) -> Void)?,
completionEventHandler: ((Subscribers.Completion<Error>) -> Void)?,
cancelEventHandler: (() -> Void)?,
method: HTTPMethod,
endpoint: (any EndpointType),
endpointName: String,
path: String,
batchEndpoints: [any EndpointType],
batchRequestVariant: Network.BatchRequest.Child.Variant,
batchResponseTypes: [Decodable.Type],
requireAllBatchResponses: Bool,
excludedSubRequestHeaders: [String],
jsonKeyedBodyEncoder: ((inout KeyedEncodingContainer<Network.BatchRequest.Child.CodingKeys>, Network.BatchRequest.Child.CodingKeys) throws -> ())?,
jsonBodyEncoder: ((inout SingleValueEncodingContainer) throws -> ())?,
b64: String?,
bytes: [UInt8]?
) {
self.request = request
self.target = target
self.originalType = originalType
self.responseType = responseType
self.retryCount = retryCount
self.timeout = timeout
self.cachedResponse = cachedResponse
self.responseConverter = responseConverter
self.subscriptionHandler = subscriptionHandler
self.outputEventHandler = outputEventHandler
self.completionEventHandler = completionEventHandler
self.cancelEventHandler = cancelEventHandler
// The following data is needed in this type for handling batch requests
self.method = method
self.endpoint = endpoint
self.endpointName = endpointName
self.path = path
self.batchEndpoints = batchEndpoints
self.batchRequestVariant = batchRequestVariant
self.batchResponseTypes = batchResponseTypes
self.requireAllBatchResponses = requireAllBatchResponses
self.excludedSubRequestHeaders = excludedSubRequestHeaders
self.jsonKeyedBodyEncoder = jsonKeyedBodyEncoder
self.jsonBodyEncoder = jsonBodyEncoder
self.b64 = b64
self.bytes = bytes
}
}
}
// MARK: - ErasedPreparedRequest
public protocol ErasedPreparedRequest {
var endpointName: String { get }
var batchRequestVariant: Network.BatchRequest.Child.Variant { get }
var batchResponseTypes: [Decodable.Type] { get }
var excludedSubRequestHeaders: [String] { get }
var erasedResponseConverter: ((ResponseInfoType, Any) throws -> Any) { get }
var erasedOutputEventHandler: ((ResponseInfoType, Any, Any) -> Void)? { get }
var completionEventHandler: ((Subscribers.Completion<Error>) -> Void)? { get }
var cancelEventHandler: (() -> Void)? { get }
func batchRequestEndpoint<E: EndpointType>(of type: E.Type) -> E?
func encodeForBatchRequest(to encoder: Encoder) throws
}
extension Network.PreparedRequest: ErasedPreparedRequest {
public var erasedResponseConverter: ((ResponseInfoType, Any) throws -> Any) {
let originalType: Decodable.Type = self.originalType
let converter: ((ResponseInfoType, Any) throws -> R) = self.responseConverter
return { info, data in
switch data {
case let subResponse as ErasedBatchSubResponse:
return Network.BatchSubResponse(
code: subResponse.code,
headers: subResponse.headers,
body: try originalType.from(subResponse.erasedBody).map { try converter(info, $0) }
)
default: return try originalType.from(data).map { try converter(info, $0) } as Any
}
}
}
public var erasedOutputEventHandler: ((ResponseInfoType, Any, Any) -> Void)? {
guard let outputEventHandler: ((CachedResponse) -> Void) = self.outputEventHandler else {
return nil
}
let originalType: Decodable.Type = self.originalType
let originalConverter: ((ResponseInfoType, Any) throws -> R) = self.responseConverter
return { info, _, data in
switch data {
case let subResponse as ErasedBatchSubResponse:
guard
let erasedBody: Any = originalType.from(subResponse.erasedBody),
let validResponse: R = try? originalConverter(info, erasedBody)
else { return }
outputEventHandler(CachedResponse(
info: info,
originalData: subResponse.erasedBody as Any,
convertedData: validResponse
))
default:
guard
let erasedBody: Any = originalType.from(data),
let validResponse: R = try? originalConverter(info, erasedBody)
else { return }
outputEventHandler(CachedResponse(
info: info,
originalData: erasedBody,
convertedData: validResponse
))
}
}
}
public func batchRequestEndpoint<E: EndpointType>(of type: E.Type) -> E? {
return (endpoint as? E)
}
public func encodeForBatchRequest(to encoder: Encoder) throws {
switch batchRequestVariant {
case .unsupported:
SNLog("Attempted to encode unsupported request type \(endpointName) as a batch subrequest")
case .sogs:
var container: KeyedEncodingContainer<Network.BatchRequest.Child.CodingKeys> = encoder.container(keyedBy: Network.BatchRequest.Child.CodingKeys.self)
// Exclude request signature headers (not used for sub-requests)
let excludedSubRequestHeaders: [String] = excludedSubRequestHeaders.map { $0.lowercased() }
let batchRequestHeaders: [String: String] = (request.allHTTPHeaderFields ?? [:])
.filter { key, _ in !excludedSubRequestHeaders.contains(key.lowercased()) }
if !batchRequestHeaders.isEmpty {
try container.encode(batchRequestHeaders, forKey: .headers)
}
try container.encode(method, forKey: .method)
try container.encode(path, forKey: .path)
try jsonKeyedBodyEncoder?(&container, .json)
try container.encodeIfPresent(b64, forKey: .b64)
try container.encodeIfPresent(bytes, forKey: .bytes)
case .storageServer:
var container: SingleValueEncodingContainer = encoder.singleValueContainer()
try jsonBodyEncoder?(&container)
}
}
}
// MARK: - Transformations
public extension Network.PreparedRequest {
func signed(
_ db: Database,
with requestSigner: (Database, Network.PreparedRequest<R>, Dependencies) throws -> URLRequest,
using dependencies: Dependencies
) throws -> Network.PreparedRequest<R> {
return Network.PreparedRequest(
request: try requestSigner(db, self, dependencies),
target: target,
originalType: originalType,
responseType: responseType,
retryCount: retryCount,
timeout: timeout,
cachedResponse: cachedResponse,
responseConverter: responseConverter,
subscriptionHandler: subscriptionHandler,
outputEventHandler: outputEventHandler,
completionEventHandler: completionEventHandler,
cancelEventHandler: cancelEventHandler,
method: method,
endpoint: endpoint,
endpointName: endpointName,
path: path,
batchEndpoints: batchEndpoints,
batchRequestVariant: batchRequestVariant,
batchResponseTypes: batchResponseTypes,
requireAllBatchResponses: requireAllBatchResponses,
excludedSubRequestHeaders: excludedSubRequestHeaders,
jsonKeyedBodyEncoder: jsonKeyedBodyEncoder,
jsonBodyEncoder: jsonBodyEncoder,
b64: b64,
bytes: bytes
)
}
/// Due to the way prepared requests work we need to cast between different types and as a result can't avoid potentially
/// throwing when mapping so the `map` function just calls through to the `tryMap` function, but we have both to make
/// the interface more consistent for dev use
func map<O>(transform: @escaping (ResponseInfoType, R) throws -> O) -> Network.PreparedRequest<O> {
return tryMap(transform: transform)
}
func tryMap<O>(transform: @escaping (ResponseInfoType, R) throws -> O) -> Network.PreparedRequest<O> {
let originalConverter: ((ResponseInfoType, Any) throws -> R) = self.responseConverter
let responseConverter: ((ResponseInfoType, Any) throws -> O) = { info, response in
let validResponse: R = try originalConverter(info, response)
return try transform(info, validResponse)
}
return Network.PreparedRequest<O>(
request: request,
target: target,
originalType: originalType,
responseType: O.self,
retryCount: retryCount,
timeout: timeout,
cachedResponse: cachedResponse.map { data in
(try? responseConverter(data.info, data.convertedData))
.map { convertedData in
Network.PreparedRequest<O>.CachedResponse(
info: data.info,
originalData: data.originalData,
convertedData: convertedData
)
}
},
responseConverter: responseConverter,
subscriptionHandler: subscriptionHandler,
outputEventHandler: self.outputEventHandler.map { eventHandler in
{ data in
guard let validResponse: R = try? originalConverter(data.info, data.originalData) else {
return
}
eventHandler(CachedResponse(
info: data.info,
originalData: data.originalData,
convertedData: validResponse
))
}
},
completionEventHandler: completionEventHandler,
cancelEventHandler: cancelEventHandler,
method: method,
endpoint: endpoint,
endpointName: endpointName,
path: path,
batchEndpoints: batchEndpoints,
batchRequestVariant: batchRequestVariant,
batchResponseTypes: batchResponseTypes,
requireAllBatchResponses: requireAllBatchResponses,
excludedSubRequestHeaders: excludedSubRequestHeaders,
jsonKeyedBodyEncoder: jsonKeyedBodyEncoder,
jsonBodyEncoder: jsonBodyEncoder,
b64: b64,
bytes: bytes
)
}
func handleEvents(
receiveSubscription: (() -> Void)? = nil,
receiveOutput: (((ResponseInfoType, R)) -> Void)? = nil,
receiveCompletion: ((Subscribers.Completion<Error>) -> Void)? = nil,
receiveCancel: (() -> Void)? = nil
) -> Network.PreparedRequest<R> {
let subscriptionHandler: (() -> Void)? = {
switch (self.subscriptionHandler, receiveSubscription) {
case (.none, .none): return nil
case (.some(let eventHandler), .none): return eventHandler
case (.none, .some(let eventHandler)): return eventHandler
case (.some(let originalEventHandler), .some(let eventHandler)):
return {
originalEventHandler()
eventHandler()
}
}
}()
let outputEventHandler: ((CachedResponse) -> Void)? = {
switch (self.outputEventHandler, receiveOutput) {
case (.none, .none): return nil
case (.some(let eventHandler), .none): return eventHandler
case (.none, .some(let eventHandler)):
return { data in
eventHandler((data.info, data.convertedData))
}
case (.some(let originalEventHandler), .some(let eventHandler)):
return { data in
originalEventHandler(data)
eventHandler((data.info, data.convertedData))
}
}
}()
let completionEventHandler: ((Subscribers.Completion<Error>) -> Void)? = {
switch (self.completionEventHandler, receiveCompletion) {
case (.none, .none): return nil
case (.some(let eventHandler), .none): return eventHandler
case (.none, .some(let eventHandler)): return eventHandler
case (.some(let originalEventHandler), .some(let eventHandler)):
return { result in
originalEventHandler(result)
eventHandler(result)
}
}
}()
let cancelEventHandler: (() -> Void)? = {
switch (self.cancelEventHandler, receiveCancel) {
case (.none, .none): return nil
case (.some(let eventHandler), .none): return eventHandler
case (.none, .some(let eventHandler)): return eventHandler
case (.some(let originalEventHandler), .some(let eventHandler)):
return {
originalEventHandler()
eventHandler()
}
}
}()
return Network.PreparedRequest(
request: request,
target: target,
originalType: originalType,
responseType: responseType,
retryCount: retryCount,
timeout: timeout,
cachedResponse: cachedResponse,
responseConverter: responseConverter,
subscriptionHandler: subscriptionHandler,
outputEventHandler: outputEventHandler,
completionEventHandler: completionEventHandler,
cancelEventHandler: cancelEventHandler,
method: method,
endpoint: endpoint,
endpointName: endpointName,
path: path,
batchEndpoints: batchEndpoints,
batchRequestVariant: batchRequestVariant,
batchResponseTypes: batchResponseTypes,
requireAllBatchResponses: requireAllBatchResponses,
excludedSubRequestHeaders: excludedSubRequestHeaders,
jsonKeyedBodyEncoder: jsonKeyedBodyEncoder,
jsonBodyEncoder: jsonBodyEncoder,
b64: b64,
bytes: bytes
)
}
}
// MARK: - Response
public extension Network.PreparedRequest {
static func cached<E: EndpointType>(
_ cachedResponse: R,
endpoint: E
) -> Network.PreparedRequest<R> where R: Decodable {
return Network.PreparedRequest(
request: URLRequest(url: URL(fileURLWithPath: "")),
target: Network.ServerTarget(server: "", endpoint: endpoint, queryParameters: [:], x25519PublicKey: ""),
originalType: R.self,
responseType: R.self,
retryCount: 0,
timeout: 0,
cachedResponse: Network.PreparedRequest<R>.CachedResponse(
info: Network.ResponseInfo(code: 0, headers: [:]),
originalData: cachedResponse,
convertedData: cachedResponse
),
responseConverter: { _, _ in cachedResponse },
subscriptionHandler: nil,
outputEventHandler: nil,
completionEventHandler: nil,
cancelEventHandler: nil,
method: .get,
endpoint: endpoint,
endpointName: E.name,
path: "",
batchEndpoints: [],
batchRequestVariant: .unsupported,
batchResponseTypes: [],
requireAllBatchResponses: false,
excludedSubRequestHeaders: [],
jsonKeyedBodyEncoder: nil,
jsonBodyEncoder: nil,
b64: nil,
bytes: nil
)
}
}
// MARK: - HTTP.PreparedRequest<R>.CachedResponse
public extension Publisher where Failure == Error {
func eraseToAnyPublisher<R>() -> AnyPublisher<(ResponseInfoType, R), Error> where Output == Network.PreparedRequest<R>.CachedResponse {
return self
.map { ($0.info, $0.convertedData) }
.eraseToAnyPublisher()
}
}
// MARK: - Decoding
public extension Decodable {
fileprivate static func from(_ value: Any?) -> Self? {
return (value as? Self)
}
static func decoded(from data: Data, using dependencies: Dependencies = Dependencies()) throws -> Self {
return try data.decoded(as: Self.self, using: dependencies)
}
}
public extension Publisher where Output == (ResponseInfoType, Data?), Failure == Error {
func decoded<R>(
with preparedRequest: Network.PreparedRequest<R>,
using dependencies: Dependencies
) -> AnyPublisher<Network.PreparedRequest<R>.CachedResponse, Error> {
self
.tryMap { responseInfo, maybeData -> Network.PreparedRequest<R>.CachedResponse in
// Depending on the 'originalType' we need to process the response differently
let targetData: Any = try {
switch preparedRequest.originalType {
case let erasedBatchResponse as ErasedBatchResponseMap.Type:
let response: Network.BatchResponse = try Network.BatchResponse.decodingResponses(
from: maybeData,
as: preparedRequest.batchResponseTypes,
requireAllResults: preparedRequest.requireAllBatchResponses,
using: dependencies
)
return try erasedBatchResponse.from(
batchEndpoints: preparedRequest.batchEndpoints,
response: response
)
case is Network.BatchResponse.Type:
return try Network.BatchResponse.decodingResponses(
from: maybeData,
as: preparedRequest.batchResponseTypes,
requireAllResults: preparedRequest.requireAllBatchResponses,
using: dependencies
)
case is NoResponse.Type: return NoResponse()
case is Optional<Data>.Type: return maybeData as Any
case is Data.Type: return try maybeData ?? { throw NetworkError.parsingFailed }()
case is _OptionalProtocol.Type:
guard let data: Data = maybeData else { return maybeData as Any }
return try preparedRequest.originalType.decoded(from: data, using: dependencies)
default:
guard let data: Data = maybeData else { throw NetworkError.parsingFailed }
return try preparedRequest.originalType.decoded(from: data, using: dependencies)
}
}()
// Generate and return the converted data
return Network.PreparedRequest<R>.CachedResponse(
info: responseInfo,
originalData: targetData,
convertedData: try preparedRequest.responseConverter(responseInfo, targetData)
)
}
.eraseToAnyPublisher()
}
}
// MARK: - _OptionalProtocol
/// This protocol should only be used within this file and is used to distinguish between `Any.Type` and `Optional<Any>.Type` as
/// it seems that `is Optional<Any>.Type` doesn't work nicely but this protocol works nicely as long as the case is under any explicit
/// `Optional<T>` handling that we need
private protocol _OptionalProtocol {}
extension Optional: _OptionalProtocol {}