Modularize HTTP code

This commit is contained in:
Justin Mazzocchi 2020-08-30 18:40:58 -07:00
parent 43f781c182
commit 71c8861600
No known key found for this signature in database
GPG key ID: E223E6937AAFB01C
34 changed files with 101 additions and 28 deletions

View file

@ -2,6 +2,7 @@
import Foundation import Foundation
import Combine import Combine
import HTTP
import Mastodon import Mastodon
// swiftlint:disable force_try // swiftlint:disable force_try

View file

@ -1,12 +1,12 @@
// Copyright © 2020 Metabolist. All rights reserved. // Copyright © 2020 Metabolist. All rights reserved.
import Foundation import Foundation
import Mastodon import HTTP
struct HTTPStubs { struct HTTPStubs {
static func stub( static func stub(
request: URLRequest, request: URLRequest,
target: HTTPTarget? = nil, target: Target? = nil,
userInfo: [String: Any] = [:]) -> HTTPStub? { userInfo: [String: Any] = [:]) -> HTTPStub? {
guard let url = request.url else { guard let url = request.url else {
return nil return nil

View file

@ -3,7 +3,7 @@
import Foundation import Foundation
import Mastodon import Mastodon
extension Target: Stubbing { extension APITarget: Stubbing {
func stub(url: URL) -> HTTPStub? { func stub(url: URL) -> HTTPStub? {
(endpoint as? Stubbing)?.stub(url: url) (endpoint as? Stubbing)?.stub(url: url)
} }

View file

@ -1,14 +1,10 @@
// Copyright © 2020 Metabolist. All rights reserved. // Copyright © 2020 Metabolist. All rights reserved.
import Foundation import Foundation
import Mastodon import HTTP
class StubbingURLProtocol: URLProtocol { class StubbingURLProtocol: URLProtocol {
private static var targetsForURLs = [URL: HTTPTarget]() private static var targetsForURLs = [URL: Target]()
class func setTarget(_ target: HTTPTarget, forURL url: URL) {
targetsForURLs[url] = target
}
override class func canInit(with task: URLSessionTask) -> Bool { override class func canInit(with task: URLSessionTask) -> Bool {
true true
@ -41,3 +37,11 @@ class StubbingURLProtocol: URLProtocol {
override func stopLoading() {} override func stopLoading() {}
} }
extension StubbingURLProtocol: TargetProcessing {
static func process(target: Target) {
if let url = try? target.asURLRequest().url {
targetsForURLs[url] = target
}
}
}

5
HTTP/.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/

27
HTTP/Package.swift Normal file
View file

@ -0,0 +1,27 @@
// swift-tools-version:5.3
import PackageDescription
let package = Package(
name: "HTTP",
platforms: [
.iOS(.v14),
.macOS(.v11)
],
products: [
.library(
name: "HTTP",
targets: ["HTTP"])
],
dependencies: [
.package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.2.2"))
],
targets: [
.target(
name: "HTTP",
dependencies: ["Alamofire"]),
.testTarget(
name: "HTTPTests",
dependencies: ["HTTP"])
]
)

View file

@ -6,7 +6,7 @@ import Alamofire
public typealias Session = Alamofire.Session public typealias Session = Alamofire.Session
public class HTTPClient { open class Client {
private let session: Session private let session: Session
private let decoder: DataDecoder private let decoder: DataDecoder
@ -15,7 +15,7 @@ public class HTTPClient {
self.decoder = decoder self.decoder = decoder
} }
public 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 as Error }.eraseToAnyPublisher() requestPublisher(target).value().mapError { $0 as Error }.eraseToAnyPublisher()
} }
@ -42,13 +42,13 @@ public class HTTPClient {
} }
} }
private extension HTTPClient { private extension Client {
func requestPublisher<T: DecodableTarget>(_ target: T) -> DataResponsePublisher<T.ResultType> { func requestPublisher<T: DecodableTarget>(_ target: T) -> DataResponsePublisher<T.ResultType> {
// #if DEBUG if let protocolClasses = session.sessionConfiguration.protocolClasses {
// if let url = try? target.asURLRequest().url { for protocolClass in protocolClasses {
// StubbingURLProtocol.setTarget(target, forURL: url) (protocolClass as? TargetProcessing.Type)?.process(target: target)
// } }
// #endif }
return session.request(target) return session.request(target)
.validate() .validate()

View file

@ -9,7 +9,7 @@ public typealias ParameterEncoding = Alamofire.ParameterEncoding
public typealias URLEncoding = Alamofire.URLEncoding public typealias URLEncoding = Alamofire.URLEncoding
public typealias JSONEncoding = Alamofire.JSONEncoding public typealias JSONEncoding = Alamofire.JSONEncoding
public protocol HTTPTarget: URLRequestConvertible { 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 }
@ -18,7 +18,7 @@ public protocol HTTPTarget: URLRequestConvertible {
var headers: HTTPHeaders? { get } var headers: HTTPHeaders? { get }
} }
public extension HTTPTarget { public extension Target {
func asURLRequest() throws -> URLRequest { func asURLRequest() throws -> URLRequest {
var url = baseURL var url = baseURL
@ -30,6 +30,10 @@ public extension HTTPTarget {
} }
} }
public protocol DecodableTarget: HTTPTarget { public protocol DecodableTarget: Target {
associatedtype ResultType: Decodable associatedtype ResultType: Decodable
} }
public protocol TargetProcessing {
static func process(target: Target)
}

View file

@ -0,0 +1,10 @@
import XCTest
@testable import HTTP
final class HTTPTests: XCTestCase {
func testExample() {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct
// results.
}
}

View file

@ -14,12 +14,12 @@ let package = Package(
targets: ["Mastodon"]) targets: ["Mastodon"])
], ],
dependencies: [ dependencies: [
.package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.2.2")) .package(path: "HTTP")
], ],
targets: [ targets: [
.target( .target(
name: "Mastodon", name: "Mastodon",
dependencies: ["Alamofire"]), dependencies: ["HTTP"]),
.testTarget( .testTarget(
name: "MastodonTests", name: "MastodonTests",
dependencies: ["Mastodon"]) dependencies: ["Mastodon"])

View file

@ -2,8 +2,9 @@
import Foundation import Foundation
import Combine import Combine
import HTTP
public final class APIClient: HTTPClient { public final class APIClient: Client {
public var instanceURL: URL? public var instanceURL: URL?
public var accessToken: String? public var accessToken: String?
@ -23,7 +24,7 @@ extension APIClient {
} }
return super.request( return super.request(
Target(baseURL: instanceURL, endpoint: endpoint, accessToken: accessToken), APITarget(baseURL: instanceURL, endpoint: endpoint, accessToken: accessToken),
decodeErrorsAs: APIError.self) decodeErrorsAs: APIError.self)
} }
} }

View file

@ -1,8 +1,9 @@
// Copyright © 2020 Metabolist. All rights reserved. // Copyright © 2020 Metabolist. All rights reserved.
import Foundation import Foundation
import HTTP
public struct Target<E: Endpoint> { public struct APITarget<E: Endpoint> {
public let baseURL: URL public let baseURL: URL
public let endpoint: E public let endpoint: E
public let accessToken: String? public let accessToken: String?
@ -14,7 +15,7 @@ public struct Target<E: Endpoint> {
} }
} }
extension Target: DecodableTarget { extension APITarget: DecodableTarget {
public typealias ResultType = E.ResultType public typealias ResultType = E.ResultType
public var pathComponents: [String] { endpoint.pathComponents } public var pathComponents: [String] { endpoint.pathComponents }

View file

@ -1,6 +1,7 @@
// Copyright © 2020 Metabolist. All rights reserved. // Copyright © 2020 Metabolist. All rights reserved.
import Foundation import Foundation
import HTTP
public protocol Endpoint { public protocol Endpoint {
associatedtype ResultType: Decodable associatedtype ResultType: Decodable

View file

@ -1,6 +1,7 @@
// Copyright © 2020 Metabolist. All rights reserved. // Copyright © 2020 Metabolist. All rights reserved.
import Foundation import Foundation
import HTTP
public enum AccessTokenEndpoint { public enum AccessTokenEndpoint {
case oauthToken( case oauthToken(

View file

@ -1,6 +1,7 @@
// Copyright © 2020 Metabolist. All rights reserved. // Copyright © 2020 Metabolist. All rights reserved.
import Foundation import Foundation
import HTTP
public enum AccountEndpoint { public enum AccountEndpoint {
case verifyCredentials case verifyCredentials

View file

@ -1,6 +1,7 @@
// Copyright © 2020 Metabolist. All rights reserved. // Copyright © 2020 Metabolist. All rights reserved.
import Foundation import Foundation
import HTTP
public enum AppAuthorizationEndpoint { public enum AppAuthorizationEndpoint {
case apps(clientName: String, redirectURI: String, scopes: String, website: URL?) case apps(clientName: String, redirectURI: String, scopes: String, website: URL?)

View file

@ -1,6 +1,7 @@
// Copyright © 2020 Metabolist. All rights reserved. // Copyright © 2020 Metabolist. All rights reserved.
import Foundation import Foundation
import HTTP
public enum ContextEndpoint { public enum ContextEndpoint {
case context(id: String) case context(id: String)

View file

@ -1,6 +1,7 @@
// Copyright © 2020 Metabolist. All rights reserved. // Copyright © 2020 Metabolist. All rights reserved.
import Foundation import Foundation
import HTTP
public enum DeletionEndpoint { public enum DeletionEndpoint {
case oauthRevoke(token: String, clientID: String, clientSecret: String) case oauthRevoke(token: String, clientID: String, clientSecret: String)

View file

@ -1,6 +1,7 @@
// Copyright © 2020 Metabolist. All rights reserved. // Copyright © 2020 Metabolist. All rights reserved.
import Foundation import Foundation
import HTTP
public enum FilterEndpoint { public enum FilterEndpoint {
case create( case create(

View file

@ -1,6 +1,7 @@
// Copyright © 2020 Metabolist. All rights reserved. // Copyright © 2020 Metabolist. All rights reserved.
import Foundation import Foundation
import HTTP
public enum FiltersEndpoint { public enum FiltersEndpoint {
case filters case filters

View file

@ -1,6 +1,7 @@
// Copyright © 2020 Metabolist. All rights reserved. // Copyright © 2020 Metabolist. All rights reserved.
import Foundation import Foundation
import HTTP
public enum InstanceEndpoint { public enum InstanceEndpoint {
case instance case instance

View file

@ -1,6 +1,7 @@
// Copyright © 2020 Metabolist. All rights reserved. // Copyright © 2020 Metabolist. All rights reserved.
import Foundation import Foundation
import HTTP
public enum ListEndpoint { public enum ListEndpoint {
case create(title: String) case create(title: String)

View file

@ -1,6 +1,7 @@
// Copyright © 2020 Metabolist. All rights reserved. // Copyright © 2020 Metabolist. All rights reserved.
import Foundation import Foundation
import HTTP
public enum ListsEndpoint { public enum ListsEndpoint {
case lists case lists

View file

@ -1,6 +1,7 @@
// Copyright © 2020 Metabolist. All rights reserved. // Copyright © 2020 Metabolist. All rights reserved.
import Foundation import Foundation
import HTTP
public struct Paged<T: Endpoint> { public struct Paged<T: Endpoint> {
public let endpoint: T public let endpoint: T

View file

@ -1,6 +1,7 @@
// Copyright © 2020 Metabolist. All rights reserved. // Copyright © 2020 Metabolist. All rights reserved.
import Foundation import Foundation
import HTTP
public enum PreferencesEndpoint { public enum PreferencesEndpoint {
case preferences case preferences

View file

@ -1,6 +1,7 @@
// Copyright © 2020 Metabolist. All rights reserved. // Copyright © 2020 Metabolist. All rights reserved.
import Foundation import Foundation
import HTTP
public enum PushSubscriptionEndpoint { public enum PushSubscriptionEndpoint {
case create( case create(

View file

@ -1,6 +1,7 @@
// Copyright © 2020 Metabolist. All rights reserved. // Copyright © 2020 Metabolist. All rights reserved.
import Foundation import Foundation
import HTTP
public enum StatusEndpoint { public enum StatusEndpoint {
case status(id: String) case status(id: String)

View file

@ -1,6 +1,7 @@
// Copyright © 2020 Metabolist. All rights reserved. // Copyright © 2020 Metabolist. All rights reserved.
import Foundation import Foundation
import HTTP
public enum TimelinesEndpoint { public enum TimelinesEndpoint {
case `public`(local: Bool) case `public`(local: Bool)

View file

@ -164,6 +164,7 @@
D0BEB20624FA1121001B0F04 /* FiltersViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiltersViewModel.swift; sourceTree = "<group>"; }; D0BEB20624FA1121001B0F04 /* FiltersViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiltersViewModel.swift; sourceTree = "<group>"; };
D0BEB21024FA2A90001B0F04 /* EditFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditFilterView.swift; sourceTree = "<group>"; }; D0BEB21024FA2A90001B0F04 /* EditFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditFilterView.swift; sourceTree = "<group>"; };
D0BEB21224FA2C0A001B0F04 /* EditFilterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditFilterViewModel.swift; sourceTree = "<group>"; }; D0BEB21224FA2C0A001B0F04 /* EditFilterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditFilterViewModel.swift; sourceTree = "<group>"; };
D0BFDAF524FC7C5300C86618 /* HTTP */ = {isa = PBXFileReference; lastKnownFileType = folder; path = HTTP; sourceTree = "<group>"; };
D0C7D41E24F76169001EBDBB /* Metatext.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Metatext.entitlements; sourceTree = "<group>"; }; D0C7D41E24F76169001EBDBB /* Metatext.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Metatext.entitlements; sourceTree = "<group>"; };
D0C7D41F24F76169001EBDBB /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; D0C7D41F24F76169001EBDBB /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
D0C7D42224F76169001EBDBB /* IdentitiesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IdentitiesView.swift; sourceTree = "<group>"; }; D0C7D42224F76169001EBDBB /* IdentitiesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IdentitiesView.swift; sourceTree = "<group>"; };
@ -292,6 +293,7 @@
D0ED1BB224CE3A1600B4899C /* Development Assets */, D0ED1BB224CE3A1600B4899C /* Development Assets */,
D0C7D46824F76169001EBDBB /* Extensions */, D0C7D46824F76169001EBDBB /* Extensions */,
D0666A7924C7745A00F3F04B /* Frameworks */, D0666A7924C7745A00F3F04B /* Frameworks */,
D0BFDAF524FC7C5300C86618 /* HTTP */,
D0C7D45624F76169001EBDBB /* Localizations */, D0C7D45624F76169001EBDBB /* Localizations */,
D0E0F1E424FC49FC002C04BF /* Mastodon */, D0E0F1E424FC49FC002C04BF /* Mastodon */,
D0C7D43824F76169001EBDBB /* Model */, D0C7D43824F76169001EBDBB /* Model */,

View file

@ -1,6 +1,7 @@
// Copyright © 2020 Metabolist. All rights reserved. // Copyright © 2020 Metabolist. All rights reserved.
import Foundation import Foundation
import HTTP
import Mastodon import Mastodon
struct AppEnvironment { struct AppEnvironment {

View file

@ -22,7 +22,7 @@ extension AuthenticationService {
redirectURI: OAuth.callbackURL.absoluteString, redirectURI: OAuth.callbackURL.absoluteString,
scopes: OAuth.scopes, scopes: OAuth.scopes,
website: OAuth.website) website: OAuth.website)
let target = Target(baseURL: instanceURL, endpoint: endpoint, accessToken: nil) let target = APITarget(baseURL: instanceURL, endpoint: endpoint, accessToken: nil)
return networkClient.request(target) return networkClient.request(target)
} }
@ -63,7 +63,7 @@ extension AuthenticationService {
grantType: OAuth.grantType, grantType: OAuth.grantType,
scopes: OAuth.scopes, scopes: OAuth.scopes,
redirectURI: OAuth.callbackURL.absoluteString) redirectURI: OAuth.callbackURL.absoluteString)
let target = Target(baseURL: instanceURL, endpoint: endpoint, accessToken: nil) let target = APITarget(baseURL: instanceURL, endpoint: endpoint, accessToken: nil)
return networkClient.request(target) return networkClient.request(target)
} }

View file

@ -3,6 +3,7 @@
import XCTest import XCTest
import Combine import Combine
import CombineExpectations import CombineExpectations
import HTTP
import Mastodon import Mastodon
@testable import Metatext @testable import Metatext