// // Copyright RevenueCat Inc. All Rights Reserved. // // Licensed under the MIT License (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://opensource.org/licenses/MIT // // HTTPResponse.swift // // Created by César de la Vega on 4/19/21. // import Foundation /// A type that represents an HTTP response protocol HTTPResponseType { associatedtype Body: HTTPResponseBody typealias Result = Swift.Result typealias Headers = [AnyHashable: Any] var httpStatusCode: HTTPStatusCode { get } /// Because this property is a standard Swift dictionary, its keys are case-sensitive. /// To perform a case-insensitive header lookup, use the `value(forHeaderField:)` method instead. var responseHeaders: HTTPClient.ResponseHeaders { get } var body: Body { get } var requestDate: Date? { get } var origin: HTTPResponseOrigin { get } } struct HTTPResponse: HTTPResponseType { var httpStatusCode: HTTPStatusCode var responseHeaders: HTTPClient.ResponseHeaders var body: Body var requestDate: Date? var origin: HTTPResponseOrigin = .backend } extension HTTPResponse: CustomStringConvertible { fileprivate var bodyDescription: String { if let bodyDescription = (self.body as? CustomStringConvertible)?.description { return bodyDescription } else { return "\(type(of: self.body))" } } var description: String { return """ HTTPResponse( statusCode: \(self.httpStatusCode.rawValue), body: \(self.bodyDescription), requestDate: \(self.requestDate?.description ?? "<>") ) """ } } // MARK: - VerifiedHTTPResponse struct VerifiedHTTPResponse: HTTPResponseType { var response: HTTPResponse var verificationResult: VerificationResult var originalSource: HTTPResponseOriginalSource init(response: HTTPResponse, verificationResult: VerificationResult, originalSource: HTTPResponseOriginalSource ) { self.response = response self.verificationResult = verificationResult self.originalSource = originalSource } init(response: HTTPResponse, verificationResult: VerificationResult, isLoadShedderResponse: Bool, isFallbackUrlResponse: Bool ) { self.init(response: response, verificationResult: verificationResult, originalSource: HTTPResponseOriginalSource(isFallbackUrlResponse: isFallbackUrlResponse, isLoadShedderResponse: isLoadShedderResponse)) } init( httpStatusCode: HTTPStatusCode, responseHeaders: HTTPClient.ResponseHeaders, body: Body, requestDate: Date? = nil, verificationResult: VerificationResult, isLoadShedderResponse: Bool, isFallbackUrlResponse: Bool ) { self.init( response: .init( httpStatusCode: httpStatusCode, responseHeaders: responseHeaders, body: body, requestDate: requestDate ), verificationResult: verificationResult, isLoadShedderResponse: isLoadShedderResponse, isFallbackUrlResponse: isFallbackUrlResponse ) } } extension VerifiedHTTPResponse: CustomStringConvertible { var description: String { return """ VerifiedHTTPResponse( statusCode: \(self.httpStatusCode.rawValue), body: \(self.response.bodyDescription), requestDate: \(self.requestDate?.description ?? "<>") verification: \(self.verificationResult) ) """ } } // MARK: - URLResponse /// `HTTPURLResponse` conformance to `HTTPResponseType` extension HTTPURLResponse: HTTPResponseType { typealias Body = Data? var httpStatusCode: HTTPStatusCode { .init(rawValue: self.statusCode) } var responseHeaders: HTTPClient.ResponseHeaders { return self.allHeaderFields } var body: Data? { return nil } var requestDate: Date? { HTTPResponse.parseRequestDate(headers: self.responseHeaders) } var origin: HTTPResponseOrigin { .backend } } // MARK: - Extensions extension HTTPResponseType { /// Equivalent to `HTTPURLResponse.value(forHTTPHeaderField:)` /// In keeping with the HTTP RFC, HTTP header field names are case-insensitive. func value(forHeaderField field: HTTPClient.ResponseHeader) -> String? { return Self.value(forCaseInsensitiveHeaderField: field, in: self.responseHeaders) } static func value( forCaseInsensitiveHeaderField field: HTTPClient.ResponseHeader, in headers: Headers ) -> String? { return Self.value(forCaseInsensitiveHeaderField: field.rawValue, in: headers) } static func value(forCaseInsensitiveHeaderField field: String, in headers: Headers) -> String? { let header = headers .first { (key, _) in (key as? String)?.caseInsensitiveCompare(field) == .orderedSame } return header?.value as? String } } extension VerifiedHTTPResponse where Body: OptionalType, Body.Wrapped: HTTPResponseBody { /// Converts a `VerifiedHTTPResponse` into a `VerifiedHTTPResponse?` var asOptionalResponse: VerifiedHTTPResponse? { guard let body = self.body.asOptional else { return nil } return self.mapBody { _ in body } } } extension HTTPResponse { func mapBody(_ mapping: (Body) throws -> NewBody) rethrows -> HTTPResponse { return .init(httpStatusCode: self.httpStatusCode, responseHeaders: self.responseHeaders, body: try mapping(self.body), requestDate: self.requestDate) } func verified( with verificationResult: VerificationResult, isLoadShedderResponse: Bool, isFallbackUrlResponse: Bool ) -> VerifiedHTTPResponse { return .init( response: self, verificationResult: verificationResult, isLoadShedderResponse: isLoadShedderResponse, isFallbackUrlResponse: isFallbackUrlResponse ) } } extension HTTPResponse { /// Creates an `HTTPResponse` extracting the `requestDate` from its headers init( httpStatusCode: HTTPStatusCode, responseHeaders: HTTPClient.ResponseHeaders, body: Body ) { self.httpStatusCode = httpStatusCode self.responseHeaders = responseHeaders self.body = body self.requestDate = Self.parseRequestDate(headers: responseHeaders) } fileprivate static func parseRequestDate(headers: HTTPResponseType.Headers) -> Date? { guard let stringValue = Self.value( forCaseInsensitiveHeaderField: HTTPClient.ResponseHeader.requestDate.rawValue, in: headers ), let intValue = UInt64(stringValue) else { return nil } return .init(millisecondsSince1970: intValue) } } extension VerifiedHTTPResponse { var httpStatusCode: HTTPStatusCode { self.response.httpStatusCode } var responseHeaders: HTTPClient.ResponseHeaders { self.response.responseHeaders } var body: Body { self.response.body } var requestDate: Date? { self.response.requestDate } var origin: HTTPResponseOrigin { self.response.origin } func mapBody(_ mapping: (Body) throws -> NewBody) rethrows -> VerifiedHTTPResponse { return .init( response: try self.response.mapBody(mapping), verificationResult: self.verificationResult, originalSource: self.originalSource ) } } enum HTTPResponseOrigin { case backend case cache } /// The server from which the HTTP response was received. enum HTTPResponseOriginalSource { case mainServer case loadShedder case fallbackUrl init(isFallbackUrlResponse: Bool, isLoadShedderResponse: Bool) { if isFallbackUrlResponse { self = .fallbackUrl } else if isLoadShedderResponse { self = .loadShedder } else { self = .mainServer } } }