Remove Alamofire

This commit is contained in:
Justin Mazzocchi 2020-09-23 00:04:37 -07:00
parent 608963567f
commit f95ecb0216
No known key found for this signature in database
GPG Key ID: E223E6937AAFB01C
20 changed files with 115 additions and 97 deletions

View File

@ -16,13 +16,11 @@ let package = Package(
name: "Stubbing", name: "Stubbing",
targets: ["Stubbing"]) targets: ["Stubbing"])
], ],
dependencies: [ dependencies: [],
.package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.2.2"))
],
targets: [ targets: [
.target( .target(
name: "HTTP", name: "HTTP",
dependencies: ["Alamofire"]), dependencies: []),
.target( .target(
name: "Stubbing", name: "Stubbing",
dependencies: ["HTTP"]), dependencies: ["HTTP"]),

View File

@ -1,22 +1,26 @@
// Copyright © 2020 Metabolist. All rights reserved. // Copyright © 2020 Metabolist. All rights reserved.
import Alamofire
import Combine import Combine
import Foundation import Foundation
public typealias Session = Alamofire.Session public enum HTTPError: Error {
case invalidStatusCode(HTTPURLResponse)
}
open class HTTPClient { open class HTTPClient {
private let session: Session private let session: URLSession
private let decoder: DataDecoder private let decoder: JSONDecoder
public init(session: Session, decoder: DataDecoder) { public init(session: URLSession, decoder: JSONDecoder) {
self.session = session self.session = session
self.decoder = decoder self.decoder = decoder
} }
open func request<T: DecodableTarget>(_ target: T) -> AnyPublisher<T.ResultType, Error> { open func request<T: DecodableTarget>(_ target: T) -> AnyPublisher<T.ResultType, Error> {
requestPublisher(target).value().mapError { $0.underlyingOrTypeErased }.eraseToAnyPublisher() dataTaskPublisher(target)
.map(\.data)
.decode(type: T.ResultType.self, decoder: decoder)
.eraseToAnyPublisher()
} }
public func request<T: DecodableTarget, E: Error & Decodable>( public func request<T: DecodableTarget, E: Error & Decodable>(
@ -24,40 +28,39 @@ open class HTTPClient {
decodeErrorsAs errorType: E.Type) -> AnyPublisher<T.ResultType, Error> { decodeErrorsAs errorType: E.Type) -> AnyPublisher<T.ResultType, Error> {
let decoder = self.decoder let decoder = self.decoder
return requestPublisher(target) return dataTaskPublisher(target)
.tryMap { response -> T.ResultType in .tryMap { result -> Data in
switch response.result { if
case let .success(decoded): return decoded let response = result.response as? HTTPURLResponse,
case let .failure(error): !Self.validStatusCodes.contains(response.statusCode) {
if
let data = response.data,
let decodedError = try? decoder.decode(E.self, from: data) {
throw decodedError
}
throw error.underlyingOrTypeErased if let decodedError = try? decoder.decode(E.self, from: result.data) {
throw decodedError
} else {
throw HTTPError.invalidStatusCode(response)
}
} }
return result.data
} }
.decode(type: T.ResultType.self, decoder: decoder)
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
} }
private extension HTTPClient { private extension HTTPClient {
func requestPublisher<T: DecodableTarget>(_ target: T) -> DataResponsePublisher<T.ResultType> { static let validStatusCodes = 200..<300
if let protocolClasses = session.sessionConfiguration.protocolClasses { func dataTaskPublisher<T: DecodableTarget>(_ target: T) -> URLSession.DataTaskPublisher {
if let protocolClasses = session.configuration.protocolClasses {
for protocolClass in protocolClasses { for protocolClass in protocolClasses {
(protocolClass as? TargetProcessing.Type)?.process(target: target) (protocolClass as? TargetProcessing.Type)?.process(target: target)
} }
} }
return session.request(target) return session.dataTaskPublisher(for: target.urlRequest())
.validate()
.publishDecodable(type: T.ResultType.self, queue: session.rootQueue, decoder: decoder)
}
}
private extension AFError { // return session.request(target.urlRequest())
var underlyingOrTypeErased: Error { // .validate()
underlyingError ?? self // .publishDecodable(type: T.ResultType.self, queue: session.rootQueue, decoder: decoder)
} }
} }

View File

@ -0,0 +1,10 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Foundation
public enum HTTPMethod: String {
case delete = "DELETE"
case get = "GET"
case post = "POST"
case put = "PUT"
}

View File

@ -1,32 +1,43 @@
// Copyright © 2020 Metabolist. All rights reserved. // Copyright © 2020 Metabolist. All rights reserved.
import Alamofire
import Foundation import Foundation
public typealias HTTPMethod = Alamofire.HTTPMethod public protocol Target {
public typealias HTTPHeaders = Alamofire.HTTPHeaders
public typealias ParameterEncoding = Alamofire.ParameterEncoding
public typealias URLEncoding = Alamofire.URLEncoding
public typealias JSONEncoding = Alamofire.JSONEncoding
public protocol Target: URLRequestConvertible {
var baseURL: URL { get } var baseURL: URL { get }
var pathComponents: [String] { get } var pathComponents: [String] { get }
var method: HTTPMethod { get } var method: HTTPMethod { get }
var encoding: ParameterEncoding { get } var queryParameters: [String: String]? { get }
var parameters: [String: Any]? { get } var jsonBody: [String: Any]? { get }
var headers: HTTPHeaders? { get } var headers: [String: String]? { get }
} }
public extension Target { public extension Target {
func asURLRequest() throws -> URLRequest { func urlRequest() -> URLRequest {
var url = baseURL var url = baseURL
for pathComponent in pathComponents { for pathComponent in pathComponents {
url.appendPathComponent(pathComponent) url.appendPathComponent(pathComponent)
} }
return try encoding.encode(try URLRequest(url: url, method: method, headers: headers), with: parameters) if var components = URLComponents(url: url, resolvingAgainstBaseURL: true),
let queryItems = queryParameters?.map(URLQueryItem.init(name:value:)) {
components.queryItems = queryItems
if let queryComponentURL = components.url {
url = queryComponentURL
}
}
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = method.rawValue
urlRequest.allHTTPHeaderFields = headers
if let jsonBody = jsonBody {
urlRequest.httpBody = try? JSONSerialization.data(withJSONObject: jsonBody)
}
return urlRequest
} }
} }

View File

@ -62,7 +62,7 @@ private extension StubbingURLProtocol {
extension StubbingURLProtocol: TargetProcessing { extension StubbingURLProtocol: TargetProcessing {
public static func process(target: Target) { public static func process(target: Target) {
if let url = try? target.asURLRequest().url { if let url = target.urlRequest().url {
targetsForURLs[url] = target targetsForURLs[url] = target
} }
} }

View File

@ -9,9 +9,9 @@ public protocol Endpoint {
var context: [String] { get } var context: [String] { get }
var pathComponentsInContext: [String] { get } var pathComponentsInContext: [String] { get }
var method: HTTPMethod { get } var method: HTTPMethod { get }
var encoding: ParameterEncoding { get } var queryParameters: [String: String]? { get }
var parameters: [String: Any]? { get } var jsonBody: [String: Any]? { get }
var headers: HTTPHeaders? { get } var headers: [String: String]? { get }
} }
public extension Endpoint { public extension Endpoint {
@ -29,14 +29,9 @@ public extension Endpoint {
context + pathComponentsInContext context + pathComponentsInContext
} }
var encoding: ParameterEncoding { var queryParameters: [String: String]? { nil }
switch method {
case .get: return URLEncoding.default
default: return JSONEncoding.default
}
}
var parameters: [String: Any]? { nil } var jsonBody: [String: Any]? { nil }
var headers: HTTPHeaders? { nil } var headers: [String: String]? { nil }
} }

View File

@ -56,7 +56,7 @@ extension AccessTokenEndpoint: Endpoint {
} }
} }
public var parameters: [String: Any]? { public var jsonBody: [String: Any]? {
switch self { switch self {
case let .oauthToken(clientID, clientSecret, grantType, scopes, code, redirectURI): case let .oauthToken(clientID, clientSecret, grantType, scopes, code, redirectURI):
var params = [ var params = [

View File

@ -23,7 +23,7 @@ extension AppAuthorizationEndpoint: Endpoint {
} }
} }
public var parameters: [String: Any]? { public var jsonBody: [String: Any]? {
switch self { switch self {
case let .apps(clientName, redirectURI, scopes, website): case let .apps(clientName, redirectURI, scopes, website):
var params = [ var params = [

View File

@ -42,7 +42,7 @@ extension DeletionEndpoint: Endpoint {
} }
} }
public var parameters: [String: Any]? { public var jsonBody: [String: Any]? {
switch self { switch self {
case let .oauthRevoke(token, clientID, clientSecret): case let .oauthRevoke(token, clientID, clientSecret):
return ["token": token, "client_id": clientID, "client_secret": clientSecret] return ["token": token, "client_id": clientID, "client_secret": clientSecret]

View File

@ -36,7 +36,7 @@ extension FilterEndpoint: Endpoint {
} }
} }
public var parameters: [String: Any]? { public var jsonBody: [String: Any]? {
switch self { switch self {
case let .create(phrase, context, irreversible, wholeWord, expiresIn): case let .create(phrase, context, irreversible, wholeWord, expiresIn):
return params(phrase: phrase, return params(phrase: phrase,

View File

@ -22,7 +22,7 @@ extension ListEndpoint: Endpoint {
} }
} }
public var parameters: [String: Any]? { public var jsonBody: [String: Any]? {
switch self { switch self {
case let .create(title): case let .create(title):
return ["title": title] return ["title": title]

View File

@ -22,6 +22,7 @@ public struct Paged<T: Endpoint> {
extension Paged: Endpoint { extension Paged: Endpoint {
public typealias ResultType = T.ResultType public typealias ResultType = T.ResultType
// public typealias ResultType = PagedResult<T.ResultType>
public var APIVersion: String { endpoint.APIVersion } public var APIVersion: String { endpoint.APIVersion }
@ -31,18 +32,25 @@ extension Paged: Endpoint {
public var method: HTTPMethod { endpoint.method } public var method: HTTPMethod { endpoint.method }
public var encoding: ParameterEncoding { endpoint.encoding } public var queryParameters: [String: String]? {
var queryParameters = endpoint.queryParameters ?? [String: String]()
public var parameters: [String: Any]? { queryParameters["max_id"] = maxID
var parameters = endpoint.parameters ?? [String: Any]() queryParameters["min_id"] = minID
queryParameters["since_id"] = sinceID
parameters["max_id"] = maxID if let limit = limit {
parameters["min_id"] = minID queryParameters["limit"] = String(limit)
parameters["since_id"] = sinceID }
parameters["limit"] = limit
return parameters return queryParameters
} }
public var headers: HTTPHeaders? { endpoint.headers } public var headers: [String: String]? { endpoint.headers }
} }
//public struct PagedResult<T: Decodable>: Decodable {
// public let result: T
// public let maxID: String?
// public let sinceID: String?
//}

View File

@ -33,7 +33,7 @@ extension PushSubscriptionEndpoint: Endpoint {
} }
} }
public var parameters: [String: Any]? { public var jsonBody: [String: Any]? {
switch self { switch self {
case let .create(endpoint, publicKey, auth, alerts): case let .create(endpoint, publicKey, auth, alerts):
return ["subscription": return ["subscription":

View File

@ -39,12 +39,14 @@ extension StatusesEndpoint: Endpoint {
} }
} }
public var parameters: [String: Any]? { public var queryParameters: [String: String]? {
switch self { switch self {
case let .timelinesPublic(local): case let .timelinesPublic(local):
return ["local": local] return ["local": String(local)]
case let .accountsStatuses(_, excludeReplies, onlyMedia, pinned): case let .accountsStatuses(_, excludeReplies, onlyMedia, pinned):
return ["exclude_replies": excludeReplies, "only_media": onlyMedia, "pinned": pinned] return ["exclude_replies": String(excludeReplies),
"only_media": String(onlyMedia),
"pinned": String(pinned)]
default: default:
return nil return nil
} }

View File

@ -9,7 +9,7 @@ public final class MastodonAPIClient: HTTPClient {
public var instanceURL: URL public var instanceURL: URL
public var accessToken: String? public var accessToken: String?
public required init(session: Session, instanceURL: URL) { public required init(session: URLSession, instanceURL: URL) {
self.instanceURL = instanceURL self.instanceURL = instanceURL
super.init(session: session, decoder: MastodonDecoder()) super.init(session: session, decoder: MastodonDecoder())
} }

View File

@ -22,19 +22,19 @@ extension MastodonAPITarget: DecodableTarget {
public var method: HTTPMethod { endpoint.method } public var method: HTTPMethod { endpoint.method }
public var encoding: ParameterEncoding { endpoint.encoding } public var queryParameters: [String: String]? { endpoint.queryParameters }
public var parameters: [String: Any]? { endpoint.parameters } public var jsonBody: [String: Any]? { endpoint.jsonBody }
public var headers: HTTPHeaders? { public var headers: [String: String]? {
var headers = endpoint.headers var headers = endpoint.headers
if let accessToken = accessToken { if let accessToken = accessToken {
if headers == nil { if headers == nil {
headers = HTTPHeaders() headers = [String: String]()
} }
headers?.add(.authorization(bearerToken: accessToken)) headers?["Authorization"] = "Bearer ".appending(accessToken)
} }
return headers return headers

View File

@ -1,15 +1,6 @@
{ {
"object": { "object": {
"pins": [ "pins": [
{
"package": "Alamofire",
"repositoryURL": "https://github.com/Alamofire/Alamofire.git",
"state": {
"branch": null,
"revision": "becd9a729a37bdbef5bc39dc3c702b99f9e3d046",
"version": "5.2.2"
}
},
{ {
"package": "CombineExpectations", "package": "CombineExpectations",
"repositoryURL": "https://github.com/groue/CombineExpectations.git", "repositoryURL": "https://github.com/groue/CombineExpectations.git",

View File

@ -8,7 +8,7 @@ import Mastodon
import UserNotifications import UserNotifications
public struct AppEnvironment { public struct AppEnvironment {
let session: Session let session: URLSession
let webAuthSessionType: WebAuthSession.Type let webAuthSessionType: WebAuthSession.Type
let keychain: Keychain.Type let keychain: Keychain.Type
let userDefaults: UserDefaults let userDefaults: UserDefaults
@ -17,7 +17,7 @@ public struct AppEnvironment {
let inMemoryContent: Bool let inMemoryContent: Bool
let fixtureDatabase: IdentityDatabase? let fixtureDatabase: IdentityDatabase?
public init(session: Session, public init(session: URLSession,
webAuthSessionType: WebAuthSession.Type, webAuthSessionType: WebAuthSession.Type,
keychain: Keychain.Type, keychain: Keychain.Type,
userDefaults: UserDefaults, userDefaults: UserDefaults,
@ -39,7 +39,7 @@ public struct AppEnvironment {
public extension AppEnvironment { public extension AppEnvironment {
static func live(userNotificationCenter: UNUserNotificationCenter) -> Self { static func live(userNotificationCenter: UNUserNotificationCenter) -> Self {
Self( Self(
session: Session(configuration: .default), session: URLSession.shared,
webAuthSessionType: LiveWebAuthSession.self, webAuthSessionType: LiveWebAuthSession.self,
keychain: LiveKeychain.self, keychain: LiveKeychain.self,
userDefaults: .standard, userDefaults: .standard,

View File

@ -73,9 +73,9 @@ private struct UpdatedFilterTarget: DecodableTarget {
let baseURL = URL(string: "https://filter.metabolist.com")! let baseURL = URL(string: "https://filter.metabolist.com")!
let pathComponents = ["filter"] let pathComponents = ["filter"]
let method = HTTPMethod.get let method = HTTPMethod.get
let encoding: ParameterEncoding = JSONEncoding.default let queryParameters: [String: String]? = nil
let parameters: [String: Any]? = nil let jsonBody: [String: Any]? = nil
let headers: HTTPHeaders? = nil let headers: [String: String]? = nil
} }
private extension InstanceURLService { private extension InstanceURLService {

View File

@ -9,7 +9,7 @@ import ServiceLayer
import Stubbing import Stubbing
public extension AppEnvironment { public extension AppEnvironment {
static func mock(session: Session = Session(configuration: .stubbing), static func mock(session: URLSession = URLSession(configuration: .stubbing),
webAuthSessionType: WebAuthSession.Type = SuccessfulMockWebAuthSession.self, webAuthSessionType: WebAuthSession.Type = SuccessfulMockWebAuthSession.self,
keychain: Keychain.Type = MockKeychain.self, keychain: Keychain.Type = MockKeychain.self,
userDefaults: UserDefaults = MockUserDefaults(), userDefaults: UserDefaults = MockUserDefaults(),
@ -18,7 +18,7 @@ public extension AppEnvironment {
inMemoryContent: Bool = true, inMemoryContent: Bool = true,
fixtureDatabase: IdentityDatabase? = nil) -> Self { fixtureDatabase: IdentityDatabase? = nil) -> Self {
AppEnvironment( AppEnvironment(
session: Session(configuration: .stubbing), session: session,
webAuthSessionType: webAuthSessionType, webAuthSessionType: webAuthSessionType,
keychain: keychain, keychain: keychain,
userDefaults: userDefaults, userDefaults: userDefaults,