Skip to content
This repository has been archived by the owner on Mar 8, 2024. It is now read-only.

Commit

Permalink
Add service-level error mapping
Browse files Browse the repository at this point in the history
  • Loading branch information
borchero committed Apr 28, 2020
1 parent 335fedb commit df63cae
Show file tree
Hide file tree
Showing 14 changed files with 172 additions and 89 deletions.
12 changes: 6 additions & 6 deletions Sources/Squid/Core/Request/HttpRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,9 @@ extension HttpRequest: CustomStringConvertible {
// MARK: Extension
extension HttpRequest {

internal static func publisher<R>(
for request: R, service: HttpService
) -> AnyPublisher<HttpRequest, Squid.Error> where R: Request {
internal static func publisher<R, S>(
for request: R, service: S
) -> AnyPublisher<HttpRequest, Squid.Error> where R: Request, S: HttpService {
return service.asyncHeader
.mapError(Squid.Error.ensure(_:))
.flatMap { header -> Future<HttpRequest, Squid.Error> in
Expand Down Expand Up @@ -129,9 +129,9 @@ extension HttpRequest {
}.eraseToAnyPublisher()
}

internal static func streamPublisher<R>(
for request: R, service: HttpService
) -> AnyPublisher<HttpRequest, Squid.Error> where R: StreamRequest {
internal static func streamPublisher<R, S>(
for request: R, service: S
) -> AnyPublisher<HttpRequest, Squid.Error> where R: StreamRequest, S: HttpService {
return service.asyncHeader
.mapError(Squid.Error.ensure(_:))
.flatMap { header -> Future<HttpRequest, Squid.Error> in
Expand Down
59 changes: 31 additions & 28 deletions Sources/Squid/Request/AnyRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ import Foundation
/// scheduling can be performed even easier.
///
/// **Note that this entity does not allow to make insecure requests over HTTP (only HTTPS).**
public struct AnyRequest: Request {
public struct AnyRequest<S: HttpService>: Request {

// MARK: Types
public typealias Result = Data

// MARK: Properties
private let service: HttpService
private let service: S

public let routes: HttpRoute

Expand All @@ -39,30 +39,6 @@ public struct AnyRequest: Request {
public let priority: RequestPriority

// MARK: Initialization
/// Initializes a new request for a particular URL.
///
/// - Parameter method: The HTTP method for the request. Defaults to GET.
/// - Parameter url: The URL of the request.
/// - Parameter query: The request's query parameters. Defaults to no parameters.
/// - Parameter header: The request's headers. Defaults to no header fields.
/// - Parameter body: The request's body. Defaults to an empty body.
/// - Parameter acceptedStatusCodes: Acceptable status codes for a successful response. Defaults
/// to all 2xx status codes.
/// - Parameter priority: The priority of the request. Defaults to `.default`.
public init(_ method: HttpMethod = .get,
url: UrlConvertible,
query: HttpQuery = [:],
header: HttpHeader = [:],
body: HttpBody = HttpData.Empty(),
acceptedStatusCodes: CountableClosedRange<Int> = 200...299,
priority: RequestPriority = .default) {
self.init(
method, routes: [], query: query, header: header, body: body,
acceptedStatusCodes: acceptedStatusCodes, priority: priority,
service: AnyHttpService(at: url)
)
}

/// Initializes a new request based on a predefined `HttpService`.
///
/// - Parameter method: The HTTP method for the request. Defaults to GET.
Expand All @@ -82,7 +58,7 @@ public struct AnyRequest: Request {
body: HttpBody = HttpData.Empty(),
acceptedStatusCodes: CountableClosedRange<Int> = 200...299,
priority: RequestPriority = .default,
service: HttpService) {
service: S) {
self.service = service
self.routes = []
self.method = method
Expand All @@ -97,7 +73,34 @@ public struct AnyRequest: Request {
/// Schedules the request and, as expected, returns a `Response` publisher. As the service is
/// transparently constructed when initializing the request, there is no need to pass a service
/// in this case. This implies that the user should **not** use the `schedule(with:)` method.
public func schedule() -> Response<Self> {
public func schedule() -> Response<Self, S> {
return self.schedule(with: self.service)
}
}

extension AnyRequest where S == AnyHttpService {

/// Initializes a new request for a particular URL.
///
/// - Parameter method: The HTTP method for the request. Defaults to GET.
/// - Parameter url: The URL of the request.
/// - Parameter query: The request's query parameters. Defaults to no parameters.
/// - Parameter header: The request's headers. Defaults to no header fields.
/// - Parameter body: The request's body. Defaults to an empty body.
/// - Parameter acceptedStatusCodes: Acceptable status codes for a successful response. Defaults
/// to all 2xx status codes.
/// - Parameter priority: The priority of the request. Defaults to `.default`.
public init(_ method: HttpMethod = .get,
url: UrlConvertible,
query: HttpQuery = [:],
header: HttpHeader = [:],
body: HttpBody = HttpData.Empty(),
acceptedStatusCodes: CountableClosedRange<Int> = 200...299,
priority: RequestPriority = .default) {
self.init(
method, routes: [], query: query, header: header, body: body,
acceptedStatusCodes: acceptedStatusCodes, priority: priority,
service: AnyHttpService(at: url)
)
}
}
28 changes: 14 additions & 14 deletions Sources/Squid/Request/Request.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ extension Request {
///
/// - Parameter service: The service representing the API against which to schedule this
/// request.
public func schedule(with service: HttpService) -> Response<Self> {
public func schedule<S>(with service: S) -> Response<Self, S> where S: HttpService {
return NetworkScheduler.shared.schedule(self, service: service)
}

Expand All @@ -122,10 +122,10 @@ extension Request {
/// - Parameter decode: A closure that is used to decode the received data to the defined type
/// of `PaginatedData`. The closure receives both the body and the request
/// as the original `Request.decode(_:)` method might want to be used.
public func schedule<P>(
forPaginationWith service: HttpService, chunk: Int, zeroBasedPageIndex: Bool = false,
public func schedule<P, S>(
forPaginationWith service: S, chunk: Int, zeroBasedPageIndex: Bool = false,
decode: @escaping (Data, Self) throws -> P
) -> Paginator<Self, P> where P: PaginatedData, P.DataType == Result {
) -> Paginator<Self, P, S> where P: PaginatedData, P.DataType == Result, S: HttpService {
return Paginator(
base: self, service: service, chunk: chunk,
zeroBasedPageIndex: zeroBasedPageIndex, decode: decode
Expand Down Expand Up @@ -223,10 +223,10 @@ extension JsonRequest {
/// against indexes the first page with 0. By default, the first
/// page is indexed by 1.
/// - Parameter paginatedType: The paginated data type to which to decode a response.
public func schedule<P>(forPaginationWith service: HttpService, chunk: Int,
zeroBasedPageIndex: Bool = false,
paginatedType: P.Type = P.self) -> Paginator<Self, P>
where P: PaginatedData, P.DataType == Result, P: Decodable {
public func schedule<P, S>(forPaginationWith service: S, chunk: Int,
zeroBasedPageIndex: Bool = false,
paginatedType: P.Type = P.self) -> Paginator<Self, P, S>
where P: PaginatedData, P.DataType == Result, P: Decodable, S: HttpService {
return Paginator(
base: self, service: service, chunk: chunk, zeroBasedPageIndex: zeroBasedPageIndex
) { data, _ -> P in
Expand All @@ -242,10 +242,10 @@ extension JsonRequest {
// MARK: Internal
extension Request {

internal func responsePublisher(
service: HttpService, session: URLSession, subject: CurrentValueSubject<URLRequest?, Never>,
internal func responsePublisher<S>(
service: S, session: URLSession, subject: CurrentValueSubject<URLRequest?, Never>,
requestId: Int
) -> AnyPublisher<Data, Squid.Error> {
) -> AnyPublisher<Data, Squid.Error> where S: HttpService {
let httpRequest = HttpRequest
.publisher(for: self, service: service)
.handleEvents(receiveOutput: { subject.send($0.urlRequest) })
Expand All @@ -266,10 +266,10 @@ extension Request {
.eraseToAnyPublisher()
}

internal func retriedResponsePublisher(
service: HttpService, session: URLSession, retrier: Retrier,
internal func retriedResponsePublisher<S>(
service: S, session: URLSession, retrier: Retrier,
subject: CurrentValueSubject<URLRequest?, Never>, requestId: Int
) -> AnyPublisher<Data, Squid.Error> {
) -> AnyPublisher<Data, Squid.Error> where S: HttpService {
let response = self.responsePublisher(
service: service, session: session, subject: subject, requestId: requestId
)
Expand Down
26 changes: 14 additions & 12 deletions Sources/Squid/Response/Paginator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,18 @@ import Combine
/// In contrast to the `Response` publisher, this instance is no publisher itself. The
/// `Paginator.connect(with:)` needs to be called which yields a publisher that emits the
/// responses for successive pagination requests. See the method's documentation for details.
public class Paginator<BaseRequestType, PaginationType>
where BaseRequestType: Request, PaginationType: PaginatedData,
public class Paginator<BaseRequestType, PaginationType, ServiceType>
where BaseRequestType: Request, PaginationType: PaginatedData, ServiceType: HttpService,
PaginationType.DataType == BaseRequestType.Result {

// MARK: Types
private let base: BaseRequestType
private let service: HttpService
private let service: ServiceType
private let chunk: Int
private let zeroBasedPageIndex: Bool
private let _decode: (Data, BaseRequestType) throws -> PaginationType

internal init(base: BaseRequestType, service: HttpService, chunk: Int, zeroBasedPageIndex: Bool,
internal init(base: BaseRequestType, service: ServiceType, chunk: Int, zeroBasedPageIndex: Bool,
decode: @escaping (Data, BaseRequestType) throws -> PaginationType) {
self.base = base
self.service = service
Expand Down Expand Up @@ -55,7 +55,9 @@ where BaseRequestType: Request, PaginationType: PaginatedData,
/// Note that this method can be called multiple times and yields independent publishers.
///
/// - Parameter ticks: The publisher that indicates the need for requesting the next page.
public func connect<P>(with ticks: P) -> AnyPublisher<BaseRequestType.Result, Squid.Error>
public func connect<P>(
with ticks: P
) -> AnyPublisher<BaseRequestType.Result, ServiceType.RequestError>
where P: Publisher, P.Failure == Never {
let conduit = PaginatorConduit(
base: self.base, service: self.service, chunk: self.chunk,
Expand All @@ -64,7 +66,7 @@ where BaseRequestType: Request, PaginationType: PaginatedData,
return ticks
.map { _ in () }
.merge(with: Just(()))
.setFailureType(to: Squid.Error.self)
.setFailureType(to: ServiceType.RequestError.self)
.filter { _ in conduit.guardState() }
.flatMap { _ in conduit.schedule() }
.extractData()
Expand All @@ -73,8 +75,8 @@ where BaseRequestType: Request, PaginationType: PaginatedData,
}
}

private class PaginatorConduit<BaseRequestType, PaginationType>
where BaseRequestType: Request, PaginationType: PaginatedData,
private class PaginatorConduit<BaseRequestType, PaginationType, ServiceType>
where BaseRequestType: Request, PaginationType: PaginatedData, ServiceType: HttpService,
PaginationType.DataType == BaseRequestType.Result {

private enum State {
Expand All @@ -86,17 +88,17 @@ where BaseRequestType: Request, PaginationType: PaginatedData,
}

typealias ScheduleType = Publishers.HandleEvents<
Response<PaginationRequest<BaseRequestType, PaginationType>>>
Response<PaginationRequest<BaseRequestType, PaginationType>, ServiceType>>

private let base: BaseRequestType
private let service: HttpService
private let service: ServiceType
private let chunk: Int
private let _decode: (Data, BaseRequestType) throws -> PaginationType

private var currentPage: Int
private var requestState = Locked<State>(.waiting)

init(base: BaseRequestType, service: HttpService, chunk: Int, zeroBasedPageIndex: Bool,
init(base: BaseRequestType, service: ServiceType, chunk: Int, zeroBasedPageIndex: Bool,
decode: @escaping (Data, BaseRequestType) throws -> PaginationType) {
self.base = base
self.service = service
Expand Down Expand Up @@ -124,7 +126,7 @@ where BaseRequestType: Request, PaginationType: PaginatedData,
}
}

func handleCompletion(_ completion: Subscribers.Completion<Squid.Error>) {
func handleCompletion(_ completion: Subscribers.Completion<ServiceType.RequestError>) {
switch completion {
case .failure:
self.requestState.value = .failed
Expand Down
5 changes: 3 additions & 2 deletions Sources/Squid/Response/Response.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ import Combine
///
/// Lastly, the class cannot be initialized by the user but is only returned by Squid upon
/// scheduling a request.
public class Response<RequestType>: Publisher where RequestType: Request {
public class Response<RequestType, ServiceType>: Publisher
where RequestType: Request, ServiceType: HttpService {

// MARK: Types
public typealias Output = RequestType.Result
public typealias Failure = Squid.Error
public typealias Failure = ServiceType.RequestError

private let publisher: AnyPublisher<Output, Failure>
private let request: RequestType
Expand Down
5 changes: 3 additions & 2 deletions Sources/Squid/Response/Stream.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ import Combine
///
/// Note that, in contrast to the `Response` publisher, this publisher does *not* replay any
/// messages received.
public class Stream<StreamRequestType>: Publisher where StreamRequestType: StreamRequest {
public class Stream<StreamRequestType, ServiceType>: Publisher
where StreamRequestType: StreamRequest, ServiceType: HttpService {

// MARK: Types
public typealias Failure = Squid.Error
public typealias Failure = ServiceType.RequestError
public typealias Output = Result<StreamRequestType.Result, Squid.Error>

private let publisher: AnyPublisher<Output, Failure>
Expand Down
10 changes: 8 additions & 2 deletions Sources/Squid/Scheduler/NetworkScheduler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ internal class NetworkScheduler {
}()

// MARK: HTTP Request
func schedule<R>(_ request: R, service: HttpService) -> Response<R> where R: Request {
func schedule<R, S>(
_ request: R, service: S
) -> Response<R, S>where R: Request, S: HttpService {
#if DEBUG
let requestId = self.runningIdentifier++
#else
Expand Down Expand Up @@ -71,13 +73,16 @@ internal class NetworkScheduler {
.map { $0.0 }
.merge(with: fulfilled)
.first()
.mapError(service.mapError(_:))
.subscribe(on: queue)

return Response(publisher: response, request: request)
}

// MARK: Web Socket Request
func schedule<R>(_ request: R, service: HttpService) -> Stream<R> where R: StreamRequest {
func schedule<R, S>(
_ request: R, service: S
) -> Stream<R, S> where R: StreamRequest, S: HttpService {
#if DEBUG
let requestId = self.runningIdentifier++
#else
Expand All @@ -102,6 +107,7 @@ internal class NetworkScheduler {
return .failure(error)
}
}.mapError(Squid.Error.ensure(_:))
.mapError(service.mapError(_:))
.subscribe(on: queue)

return Stream(publisher: response, task: socket, request: request)
Expand Down
16 changes: 16 additions & 0 deletions Sources/Squid/Service/HttpService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ import Combine
/// represents a particular API.
public protocol HttpService {

// MARK: Associated Types
/// The type of error that requests scheduled against this service emits. Defaults to
/// `Squid.Error`.
associatedtype RequestError: Error = Squid.Error

// MARK: API Configuration
/// The URL of the API representes by this HTTP service (e.g. "api.example.com"). This is the
/// only field that needs to be provided by a particular implementation. This url should not
Expand Down Expand Up @@ -42,6 +47,9 @@ public protocol HttpService {
/// - Note: When scheduling a `StreamRequest`, retriers will be ignored.
var retrierFactory: RetrierFactory { get }

/// Maps errors retrieved from requests to this service's custom error.
func mapError(_ error: Squid.Error) -> RequestError

// MARK: Hooks
/// The hook describes a component that is called whenever a request is scheduled for this
/// service and a result was obtained for a request. If an error occurs during scheduling,
Expand Down Expand Up @@ -80,3 +88,11 @@ extension HttpService {
return NilServiceHook()
}
}

extension HttpService where RequestError == Squid.Error {

/// If the default error type `Squid.Error` is used, it is simply returned without modification.
public func mapError(_ error: Squid.Error) -> RequestError {
return error
}
}
Loading

0 comments on commit df63cae

Please sign in to comment.