Created
December 8, 2020 18:32
-
-
Save luizmb/b200323111fdbe4ab0e85b0853687fb5 to your computer and use it in GitHub Desktop.
How to write a basic HTTP API in Swift using Combine
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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