// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. import Foundation import Combine import GRDB // MARK: - HTTPRequestMetadata public typealias HTTPRequestMetadata = String // MARK: - HTTP.PreparedRequest public extension HTTP { struct PreparedRequest { public let request: URLRequest public let server: String public let publicKey: String public let originalType: Decodable.Type public let responseType: R.Type public let metadata: [HTTPRequestMetadata: Any] public let retryCount: Int public let timeout: TimeInterval fileprivate let responseConverter: ((ResponseInfoType, Any) throws -> R) public let outputEventHandler: (((ResponseInfoType, R)) -> Void)? public let completionEventHandler: ((Subscribers.Completion) -> 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: HTTP.BatchRequest.Child.Variant public let batchResponseTypes: [Decodable.Type] public let excludedSubRequestHeaders: [String] /// The `jsonBodyEncoder` is used to simplify the encoding for `BatchRequest` private let jsonBodyEncoder: ((inout KeyedEncodingContainer, HTTP.BatchRequest.Child.CodingKeys) throws -> ())? private let b64: String? private let bytes: [UInt8]? public init( request: Request, urlRequest: URLRequest, publicKey: String, responseType: R.Type, metadata: [HTTPRequestMetadata: Any] = [:], retryCount: Int = 0, timeout: TimeInterval ) where R: Decodable { let batchRequests: [HTTP.BatchRequest.Child]? = (request.body as? HTTP.BatchRequest)?.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.server = request.server self.publicKey = publicKey self.originalType = responseType self.responseType = responseType self.metadata = metadata self.retryCount = retryCount self.timeout = timeout self.responseConverter = { _, response in guard let validResponse: R = response as? R else { throw HTTPError.invalidResponse } return validResponse } // 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.outputEventHandler = { guard let subRequestEventHandlers: [(Int, (((ResponseInfoType, 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.1 { case let batchResponse as HTTP.BatchResponse: subRequestEventHandlers.forEach { index, eventHandler in guard batchResponse.count > index else { SNLog("[PreparedRequest] Unable to handle output events for missing response") return } eventHandler((data.0, batchResponse[index])) } case let batchResponseMap as HTTP.BatchResponseMap: subRequestEventHandlers.forEach { index, eventHandler in guard batchEndpoints.count > index, let targetResponse: Decodable = batchResponseMap[batchEndpoints[index]] else { SNLog("[PreparedRequest] Unable to handle output events for missing response") return } eventHandler((data.0, targetResponse)) } default: SNLog("[PreparedRequest] Unable to handle output events for unknown batch response type") } } }() self.completionEventHandler = { guard let subRequestEventHandlers: [((Subscribers.Completion) -> 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.self)" self.path = request.urlPathAndParamsString self.batchEndpoints = batchEndpoints self.batchRequestVariant = E.batchRequestVariant self.batchResponseTypes = batchResponseTypes.defaulting(to: [HTTP.BatchSubResponse.self]) 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.jsonBodyEncoder = nil self.b64 = bodyString self.bytes = nil case let bodyBytes as [UInt8]: self.jsonBodyEncoder = nil self.b64 = nil self.bytes = bodyBytes default: self.jsonBodyEncoder = { [body = request.body] container, key in try container.encodeIfPresent(body, forKey: key) } self.b64 = nil self.bytes = nil } } fileprivate init( request: URLRequest, server: String, publicKey: String, originalType: U.Type, responseType: R.Type, metadata: [HTTPRequestMetadata: Any], retryCount: Int, timeout: TimeInterval, responseConverter: @escaping (ResponseInfoType, Any) throws -> R, outputEventHandler: (((ResponseInfoType, R)) -> Void)?, completionEventHandler: ((Subscribers.Completion) -> Void)?, cancelEventHandler: (() -> Void)?, method: HTTPMethod, endpoint: (any EndpointType), endpointName: String, path: String, batchEndpoints: [any EndpointType], batchRequestVariant: HTTP.BatchRequest.Child.Variant, batchResponseTypes: [Decodable.Type], excludedSubRequestHeaders: [String], jsonBodyEncoder: ((inout KeyedEncodingContainer, HTTP.BatchRequest.Child.CodingKeys) throws -> ())?, b64: String?, bytes: [UInt8]? ) { self.request = request self.server = server self.publicKey = publicKey self.originalType = originalType self.responseType = responseType self.metadata = metadata self.retryCount = retryCount self.timeout = timeout self.responseConverter = responseConverter 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.excludedSubRequestHeaders = excludedSubRequestHeaders self.jsonBodyEncoder = jsonBodyEncoder self.b64 = b64 self.bytes = bytes } } } // MARK: - ErasedPreparedRequest public protocol ErasedPreparedRequest { var endpointName: String { get } var batchRequestVariant: HTTP.BatchRequest.Child.Variant { get } var batchResponseTypes: [Decodable.Type] { get } var excludedSubRequestHeaders: [String] { get } var erasedOutputEventHandler: (((ResponseInfoType, Any)) -> Void)? { get } var completionEventHandler: ((Subscribers.Completion) -> Void)? { get } var cancelEventHandler: (() -> Void)? { get } func batchRequestEndpoint(of type: E.Type) -> E? func encodeForBatchRequest(to encoder: Encoder) throws } extension HTTP.PreparedRequest: ErasedPreparedRequest { public var erasedOutputEventHandler: (((ResponseInfoType, Any)) -> Void)? { guard let outputEventHandler: (((ResponseInfoType, R)) -> Void) = self.outputEventHandler else { return nil } return { data in guard let subResponse: HTTP.BatchSubResponse = data.1 as? HTTP.BatchSubResponse else { guard let directResponse: R = data.1 as? R else { return } outputEventHandler((data.0, directResponse)) return } guard let value: R = subResponse.body else { return } outputEventHandler((subResponse, value)) } } public func batchRequestEndpoint(of type: E.Type) -> E? { return (endpoint as? E) } public func encodeForBatchRequest(to encoder: Encoder) throws { var container: KeyedEncodingContainer = encoder.container(keyedBy: HTTP.BatchRequest.Child.CodingKeys.self) switch batchRequestVariant { case .unsupported: SNLog("Attempted to encode unsupported request type \(endpointName) as a batch subrequest") case .sogs: // 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 jsonBodyEncoder?(&container, .json) try container.encodeIfPresent(b64, forKey: .b64) try container.encodeIfPresent(bytes, forKey: .bytes) case .storageServer: try container.encode(method, forKey: .method) try jsonBodyEncoder?(&container, .params) } } } // MARK: - Transformations public extension HTTP.PreparedRequest { func signed( _ db: Database, with requestSigner: (Database, HTTP.PreparedRequest, Dependencies) throws -> URLRequest, using dependencies: Dependencies ) throws -> HTTP.PreparedRequest { return HTTP.PreparedRequest( request: try requestSigner(db, self, dependencies), server: server, publicKey: publicKey, originalType: originalType, responseType: responseType, metadata: metadata, retryCount: retryCount, timeout: timeout, responseConverter: responseConverter, outputEventHandler: outputEventHandler, completionEventHandler: completionEventHandler, cancelEventHandler: cancelEventHandler, method: method, endpoint: endpoint, endpointName: endpointName, path: path, batchEndpoints: batchEndpoints, batchRequestVariant: batchRequestVariant, batchResponseTypes: batchResponseTypes, excludedSubRequestHeaders: excludedSubRequestHeaders, jsonBodyEncoder: jsonBodyEncoder, b64: b64, bytes: bytes ) } func map(transform: @escaping (ResponseInfoType, R) throws -> O) -> HTTP.PreparedRequest { let originalResponseConverter: ((ResponseInfoType, Any) throws -> R) = self.responseConverter let responseConverter: ((ResponseInfoType, Any) throws -> O) = { info, response in let validResponse: R = try originalResponseConverter(info, response) return try transform(info, validResponse) } return HTTP.PreparedRequest( request: request, server: server, publicKey: publicKey, originalType: originalType, responseType: O.self, metadata: metadata, retryCount: retryCount, timeout: timeout, responseConverter: responseConverter, outputEventHandler: self.outputEventHandler.map { eventHandler in { data in guard let validResponse: R = try? originalResponseConverter(data.0, data.1) else { return } eventHandler((data.0, validResponse)) } }, completionEventHandler: completionEventHandler, cancelEventHandler: cancelEventHandler, method: method, endpoint: endpoint, endpointName: endpointName, path: path, batchEndpoints: batchEndpoints, batchRequestVariant: batchRequestVariant, batchResponseTypes: batchResponseTypes, excludedSubRequestHeaders: excludedSubRequestHeaders, jsonBodyEncoder: jsonBodyEncoder, b64: b64, bytes: bytes ) } func handleEvents( receiveOutput: (((ResponseInfoType, R)) -> Void)? = nil, receiveCompletion: ((Subscribers.Completion) -> Void)? = nil, receiveCancel: (() -> Void)? = nil ) -> HTTP.PreparedRequest { let outputEventHandler: (((ResponseInfoType, R)) -> Void)? = { switch (self.outputEventHandler, receiveOutput) { 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 { data in originalEventHandler(data) eventHandler(data) } } }() let completionEventHandler: ((Subscribers.Completion) -> 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 HTTP.PreparedRequest( request: request, server: server, publicKey: publicKey, originalType: originalType, responseType: responseType, metadata: metadata, retryCount: retryCount, timeout: timeout, responseConverter: responseConverter, outputEventHandler: outputEventHandler, completionEventHandler: completionEventHandler, cancelEventHandler: cancelEventHandler, method: method, endpoint: endpoint, endpointName: endpointName, path: path, batchEndpoints: batchEndpoints, batchRequestVariant: batchRequestVariant, batchResponseTypes: batchResponseTypes, excludedSubRequestHeaders: excludedSubRequestHeaders, jsonBodyEncoder: jsonBodyEncoder, b64: b64, bytes: bytes ) } } // MARK: - Decoding public extension Decodable { 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( with preparedRequest: HTTP.PreparedRequest, using dependencies: Dependencies ) -> AnyPublisher<(ResponseInfoType, R), Error> { self .tryMap { responseInfo, maybeData -> (ResponseInfoType, R) 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 responses: HTTP.BatchResponse = try HTTP.BatchResponse.decodingResponses( from: maybeData, as: preparedRequest.batchResponseTypes, requireAllResults: true, using: dependencies ) return try erasedBatchResponse.from( batchEndpoints: preparedRequest.batchEndpoints, responses: responses ) case is NoResponse.Type: return NoResponse() case is Optional.Type: return maybeData as Any case is Data.Type: return try maybeData ?? { throw HTTPError.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 HTTPError.parsingFailed } return try preparedRequest.originalType.decoded(from: data, using: dependencies) } }() // Generate and return the converted data let convertedData: R = try preparedRequest.responseConverter(responseInfo, targetData) return (responseInfo, convertedData) } .eraseToAnyPublisher() } } // MARK: - _OptionalProtocol /// This protocol should only be used within this file and is used to distinguish between `Any.Type` and `Optional.Type` as /// it seems that `is Optional.Type` doesn't work nicely but this protocol works nicely as long as the case is under any explicit /// `Optional` handling that we need private protocol _OptionalProtocol {} extension Optional: _OptionalProtocol {}