Skip to content

Instantly share code, notes, and snippets.

@luizmb
Created December 8, 2020 18:32
Show Gist options
  • Save luizmb/b200323111fdbe4ab0e85b0853687fb5 to your computer and use it in GitHub Desktop.
Save luizmb/b200323111fdbe4ab0e85b0853687fb5 to your computer and use it in GitHub Desktop.
How to write a basic HTTP API in Swift using Combine
import Combine
public enum APIError: Error {
case urlError(URLError)
case invalidResponse(URLResponse)
case decodingError(DecodingError)
case unknownError(Error)
}
extension Publisher where Output == (data: Data, response: URLResponse), Failure == URLError {
public func decode<T: Decodable, Decoder: TopLevelDecoder>(type: T.Type, decoder: Decoder) -> AnyPublisher<T, APIError>
where Decoder.Input == Data {
mapError(APIError.urlError)
.flatMapResult(Self.ensureStatusCode)
.flatMapResult(Self.decodeSync(type: type, decoder: decoder))
.eraseToAnyPublisher()
}
private static func ensureStatusCode(data: Data, urlResponse: URLResponse) -> Result<Data, APIError> {
guard let httpURLResponse = urlResponse as? HTTPURLResponse,
(200 ..< 300) ~= httpURLResponse.statusCode else {
return .failure(.invalidResponse(urlResponse))
}
return .success(data)
}
private static func decodeSync<T: Decodable, Decoder: TopLevelDecoder>(type: T.Type, decoder: Decoder) -> (Data) -> Result<T, APIError>
where Decoder.Input == Data {
return { data in
decoder
.decodeResult(type, from: data)
.mapError { error in
if let decodingError = error as? DecodingError { return .decodingError(decodingError) }
return .unknownError(error)
}
}
}
}
extension TopLevelDecoder {
/// Same as `decode`, but using Result instead of `throws`
/// - Parameters:
/// - type: Swift Type to be decoded into
/// - data: raw encoded data in whatever `Input` format this `TopLevelDecoder` expects
/// - Returns: `Result.success` of `T` in case of successful decode, otherwise `Result.failure` of `Error`
public func decodeResult<T: Decodable>(_ type: T.Type, from data: Input) -> Result<T, Error> {
Result {
try decode(type, from: data)
}
}
}
extension Publisher {
/// Same as flatMap, but instead of expecting return of a Publisher, a Result is enough. Result will be eventually lifted into Publisher
/// by using `Result.publisher` property
/// - Parameter transform: maps the `Output` value into a `Result` of either `NewOutput` for success, or `Failure` for error
/// - Returns: A Publisher that represents the flatMap version of the provided `Result` operation
public func flatMapResult<NewOutput>(_ transform: @escaping (Output) -> Result<NewOutput, Failure>)
-> Publishers.FlatMap<Result<NewOutput, Failure>.Publisher, Self> {
flatMap { output -> Result<NewOutput, Failure>.Publisher in
transform(output).publisher
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment