|
| 1 | +import Foundation |
| 2 | + |
| 3 | +/// `HTTPClient` creates and manages requests over the network. |
| 4 | +/// |
| 5 | +/// The client also provides common functionality for all ``HTTPRequest`` objects, including encoding and decoding strategies, |
| 6 | +/// request adaptation, response validation and retry stratagies. |
| 7 | +/// |
| 8 | +/// Generally an ``HTTPClient`` is used to manage the interaction with a single API service. Most APIs |
| 9 | +/// have their own nuance and complexities, and encapsulating all of that in one place can help structure your code in a |
| 10 | +/// more scalable and testable way. |
| 11 | +/// |
| 12 | +/// If your API requires a custom encoder or decoder, you can provide your own custom ones to the client. These encoder and decoders |
| 13 | +/// will then be used for all requests made using the client. |
| 14 | +/// |
| 15 | +/// If your API requires some sort of repetitive task applied to each request, rather than manipulating it at each call, you can make use of an |
| 16 | +/// ``HTTPRequestAdaptor``. The adaptors that you provide to the client will all be run on a request before it is sent to your API. |
| 17 | +/// This provides you with an opportunity to insert various headers or perform asyncrounous tasks before sending the outbound request. |
| 18 | +/// |
| 19 | +/// Different APIs require different forms of validation that help you determine if a request was successful or not. |
| 20 | +/// The default ``HTTPClient`` makes no assumptions about the format and/or success of an API's response. Instead, you can make use of an |
| 21 | +/// ``HTTPResponseValidator``. The validators that you provide the client will all be run on the response received by your API. |
| 22 | +/// This provides you with an opportunity to determine wether or not the response was a success and failure, and consolidate potentially repeated logic in one place. |
| 23 | +/// |
| 24 | +/// Sometimes you may want to implement logic for handling retries of failed requests. An ``HTTPClient`` can make use of an |
| 25 | +/// ``HTTPRequestRetriers``. The retriers that you provide the client will be run when a request fails. |
| 26 | +/// This provides you with an opportunity to observe the error and determine wether or not the request should be sent again. You can implement complex retry |
| 27 | +/// logic across your entire client without having to repeat yourself. |
| 28 | +/// |
| 29 | +/// In addition to providing adaptors, validators and retriers to your entire client, you can provide additional configuration to each |
| 30 | +/// request using a chaining style syntax. This allows you to add additional configuration to endpoints that you may want to provide further |
| 31 | +/// customization to beyond the standard configuration at the client level. |
| 32 | +/// |
| 33 | +/// ```swift |
| 34 | +/// // Create a client. |
| 35 | +/// let client = HTTPClient() |
| 36 | +/// |
| 37 | +/// // Create a request. |
| 38 | +/// let request = client.request(for: .get, to: url, expecting: [Dog].self) |
| 39 | +/// .adapt(with: adaptor) |
| 40 | +/// .validate(with: validator) |
| 41 | +/// .retry(with: retrier) |
| 42 | +/// |
| 43 | +/// // Send a request. |
| 44 | +/// let response = try await request.run() |
| 45 | +/// ``` |
| 46 | +/// |
| 47 | +/// > Note: All adaptors, validators and retriers at the client level will be run first, after which the |
| 48 | +/// request configurations at the request level will be run. |
| 49 | +/// |
| 50 | +/// > Important: All adaptors will always run for every request. |
| 51 | +/// |
| 52 | +/// > Important: Validators will be run one by one on a request's response. If any validators determines that the response is invalid, |
| 53 | +/// the request will fail and throw the error returned by that validator. No other subsequent validators will be run for that run of a request. |
| 54 | +/// |
| 55 | +/// > Important: Retriers will be run one by one in the event of a failred request. If any retrier concedes that the request should not be retried, |
| 56 | +/// the request will ask the next retrier if the request should be retried. If any retrier determines that a request should be retired, the request will |
| 57 | +/// immedietly be retired, without asking any subsequent retriers. |
| 58 | +public struct HTTPClient { |
| 59 | + |
| 60 | + // MARK: Properties |
| 61 | + |
| 62 | + /// The encoder that each request uses to encode request bodies. |
| 63 | + public let encoder: JSONEncoder |
| 64 | + |
| 65 | + /// The decoder that each request uses to decode response bodies. |
| 66 | + public let decoder: JSONDecoder |
| 67 | + |
| 68 | + /// The dispatcher that sends out each request. |
| 69 | + public let dispatcher: HTTPDispatcher |
| 70 | + |
| 71 | + /// A collection of adaptors that adapt each request. |
| 72 | + public let adaptors: [any HTTPRequestAdaptor] |
| 73 | + |
| 74 | + /// A collection of retriers that handle retrying failed requests. |
| 75 | + public let retriers: [any HTTPRequestRetrier] |
| 76 | + |
| 77 | + /// A collection of validators that validate each response. |
| 78 | + public let validators: [any HTTPResponseValidator] |
| 79 | + |
| 80 | + // MARK: Initializers |
| 81 | + |
| 82 | + /// Creates an ``HTTPClient`` with the provided configuration. |
| 83 | + /// |
| 84 | + /// - Parameters: |
| 85 | + /// - encoder: The encoder that each request should use to encode request bodies. |
| 86 | + /// - decoder: The decoder that each request should use to decode response bodies. |
| 87 | + /// - dispatcher: The dispatcher that will actually send out each request. |
| 88 | + /// - adaptors: A collection of adaptors that adapt each request. |
| 89 | + /// - retriers: A collection of retriers that handle retrying failed requests. |
| 90 | + /// - validators: A collection of validators that validate each response. |
| 91 | + public init( |
| 92 | + encoder: JSONEncoder = JSONEncoder(), |
| 93 | + decoder: JSONDecoder = JSONDecoder(), |
| 94 | + dispatcher: HTTPDispatcher = .live(), |
| 95 | + adaptors: [any HTTPRequestAdaptor] = [], |
| 96 | + retriers: [any HTTPRequestRetrier] = [], |
| 97 | + validators: [any HTTPResponseValidator] = [] |
| 98 | + ) { |
| 99 | + self.encoder = encoder |
| 100 | + self.decoder = decoder |
| 101 | + self.dispatcher = dispatcher |
| 102 | + self.adaptors = adaptors |
| 103 | + self.retriers = retriers |
| 104 | + self.validators = validators |
| 105 | + } |
| 106 | + |
| 107 | + // MARK: Public |
| 108 | + |
| 109 | + /// Creates a request with a body. |
| 110 | + /// |
| 111 | + /// - Parameters: |
| 112 | + /// - method: The ``HTTPMethod`` of the request. |
| 113 | + /// - url: The url the request is being sent to. |
| 114 | + /// - body: The object to encode into the body of the request. |
| 115 | + /// - responseType: The expected type to be decoded from the response body. |
| 116 | + /// - Returns: An `HTTPRequest` with the provided configuration. |
| 117 | + public func request<T: Encodable, U: Decodable>( |
| 118 | + for method: HTTPMethod, |
| 119 | + to url: URL, |
| 120 | + with body: T, |
| 121 | + expecting responseType: U.Type |
| 122 | + ) throws -> HTTPRequest<U> { |
| 123 | + request( |
| 124 | + method: method, |
| 125 | + url: url, |
| 126 | + body: try encoder.encode(body), |
| 127 | + responseType: responseType |
| 128 | + ) |
| 129 | + } |
| 130 | + |
| 131 | + /// Creates a request without a body. |
| 132 | + /// |
| 133 | + /// - Parameters: |
| 134 | + /// - method: The ``HTTPMethod`` of the request. |
| 135 | + /// - url: The url the request is being sent to. |
| 136 | + /// - responseType: The expected type to be decoded from the response body. |
| 137 | + /// - Returns: An `HTTPRequest` with the provided configuration. |
| 138 | + public func request<T: Decodable>( |
| 139 | + for method: HTTPMethod, |
| 140 | + to url: URL, |
| 141 | + expecting responseType: T.Type |
| 142 | + ) -> HTTPRequest<T> { |
| 143 | + return request( |
| 144 | + method: method, |
| 145 | + url: url, |
| 146 | + body: nil, |
| 147 | + responseType: responseType |
| 148 | + ) |
| 149 | + } |
| 150 | + |
| 151 | + // MARK: Private |
| 152 | + |
| 153 | + /// Creates a request with a body. |
| 154 | + /// |
| 155 | + /// - Parameters: |
| 156 | + /// - method: The ``HTTPMethod`` of the request. |
| 157 | + /// - url: The url the request is being sent to. |
| 158 | + /// - body: The data to attach to the request's body. |
| 159 | + /// - responseType: The expected type to be decoded from the response body. |
| 160 | + /// - Returns: An `HTTPRequest` with the provided configuration. |
| 161 | + private func request<T: Decodable>( |
| 162 | + method: HTTPMethod, |
| 163 | + url: URL, |
| 164 | + body: Data?, |
| 165 | + responseType: T.Type |
| 166 | + ) -> HTTPRequest<T> { |
| 167 | + var request = URLRequest(url: url) |
| 168 | + |
| 169 | + if body != nil { |
| 170 | + assert(method != .get,"GET requests should not contain a body.") |
| 171 | + } |
| 172 | + |
| 173 | + request.httpMethod = method.rawValue |
| 174 | + request.httpBody = body |
| 175 | + return HTTPRequest( |
| 176 | + request: request, |
| 177 | + decoder: decoder, |
| 178 | + dispatcher: dispatcher, |
| 179 | + adaptors: adaptors, |
| 180 | + retriers: retriers, |
| 181 | + validators: validators |
| 182 | + ) |
| 183 | + } |
| 184 | +} |
0 commit comments