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 Combine
import HTTP
import Mastodon
// swiftlint:disable force_try

View file

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

View file

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

View file

@ -1,14 +1,10 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Foundation
import Mastodon
import HTTP
class StubbingURLProtocol: URLProtocol {
private static var targetsForURLs = [URL: HTTPTarget]()
class func setTarget(_ target: HTTPTarget, forURL url: URL) {
targetsForURLs[url] = target
}
private static var targetsForURLs = [URL: Target]()
override class func canInit(with task: URLSessionTask) -> Bool {
true
@ -41,3 +37,11 @@ class StubbingURLProtocol: URLProtocol {
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 class HTTPClient {
open class Client {
private let session: Session
private let decoder: DataDecoder
@ -15,7 +15,7 @@ public class HTTPClient {
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()
}
@ -42,13 +42,13 @@ public class HTTPClient {
}
}
private extension HTTPClient {
private extension Client {
func requestPublisher<T: DecodableTarget>(_ target: T) -> DataResponsePublisher<T.ResultType> {
// #if DEBUG
// if let url = try? target.asURLRequest().url {
// StubbingURLProtocol.setTarget(target, forURL: url)
// }
// #endif
if let protocolClasses = session.sessionConfiguration.protocolClasses {
for protocolClass in protocolClasses {
(protocolClass as? TargetProcessing.Type)?.process(target: target)
}
}
return session.request(target)
.validate()

View file

@ -9,7 +9,7 @@ public typealias ParameterEncoding = Alamofire.ParameterEncoding
public typealias URLEncoding = Alamofire.URLEncoding
public typealias JSONEncoding = Alamofire.JSONEncoding
public protocol HTTPTarget: URLRequestConvertible {
public protocol Target: URLRequestConvertible {
var baseURL: URL { get }
var pathComponents: [String] { get }
var method: HTTPMethod { get }
@ -18,7 +18,7 @@ public protocol HTTPTarget: URLRequestConvertible {
var headers: HTTPHeaders? { get }
}
public extension HTTPTarget {
public extension Target {
func asURLRequest() throws -> URLRequest {
var url = baseURL
@ -30,6 +30,10 @@ public extension HTTPTarget {
}
}
public protocol DecodableTarget: HTTPTarget {
public protocol DecodableTarget: Target {
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"])
],
dependencies: [
.package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.2.2"))
.package(path: "HTTP")
],
targets: [
.target(
name: "Mastodon",
dependencies: ["Alamofire"]),
dependencies: ["HTTP"]),
.testTarget(
name: "MastodonTests",
dependencies: ["Mastodon"])

View file

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

View file

@ -1,8 +1,9 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Foundation
import HTTP
public struct Target<E: Endpoint> {
public struct APITarget<E: Endpoint> {
public let baseURL: URL
public let endpoint: E
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 var pathComponents: [String] { endpoint.pathComponents }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -164,6 +164,7 @@
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>"; };
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>"; };
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>"; };
@ -292,6 +293,7 @@
D0ED1BB224CE3A1600B4899C /* Development Assets */,
D0C7D46824F76169001EBDBB /* Extensions */,
D0666A7924C7745A00F3F04B /* Frameworks */,
D0BFDAF524FC7C5300C86618 /* HTTP */,
D0C7D45624F76169001EBDBB /* Localizations */,
D0E0F1E424FC49FC002C04BF /* Mastodon */,
D0C7D43824F76169001EBDBB /* Model */,

View file

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

View file

@ -22,7 +22,7 @@ extension AuthenticationService {
redirectURI: OAuth.callbackURL.absoluteString,
scopes: OAuth.scopes,
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)
}
@ -63,7 +63,7 @@ extension AuthenticationService {
grantType: OAuth.grantType,
scopes: OAuth.scopes,
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)
}

View file

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