端点 (Endpoints)

Build Status Swift Package Manager compatible

Endpoints 是基于 URLSession 的一个轻量级网络抽象层,它能让你在几秒钟内启动并运行你的网络代码。

import Endpoints

struct MyEndpoint: Endpoint {
    let baseURL: URL = URL(string: "https://mysite.com/api")!
    let path: String = "/"
    let method: HTTPMethod = .get
}

let endpoint = MyEndpoint()
let communicator = Communicator()
communicator.performRequest(to: endpoint) { result in
    switch result {
    case .success(let response):
        // Yay!
        break
    case .failure(let error):
        // Nay...
        break
    }
}

用法

在许多情况下,你可能对请求的实际响应数据感兴趣。Endpoints 使用关联的 ResponseType 类型,使解码 Swift struct 变得简单。

// Define your model
struct MyModel: Decodable {
    let id: Int
    let name: String
}

struct MyEndpoint: Endpoint {
    // Declare a ResponseType in your endpoint
    typealias ResponseType = MyModel

    let baseURL: URL = URL(string: "https://mysite.com/api")!
    let path: String = "/mymodel/1"
    let method: HTTPMethod = .get
}

communicator.performRequest(to: endpoint) { result in
    switch result {
    case .success(let response):
        // response.body contains an instance of MyModel
        break
    case .failure(let error):
        break
    }
}

默认情况下,Endpoints 假定 JSON 解码。如果你需要更改默认的解码行为,请查看“通过扩展进行自定义”部分。

Combine

Endpoints 还内置支持 Apple 的 Combine 框架。

communicator.publisher(for: endpoint)
    .sink(
        receiveCompletion: { completion in
            // Handle successful completion or failure
        },
        receiveValue: { response in
            // Handle a successful response here
        }
    )

通过扩展进行自定义

如果你需要更改端点的默认解码行为,你可以简单地覆盖默认实现。

extension Endpoint where ResponseType: Decodable {
    func unpack(data: Data) throws -> ResponseType {
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .secondsSince1970
        decoder.keyDecodingStrategy = .convertFromSnakeCase

        return try decoder.decode(ResponseType.self, from: data)
    }
}

如果你想混合搭配,你可以保持默认设置不变,并在你的 Endpoint 实现中覆盖 unpack(data:) 函数以提供自定义解码。

struct MyEndpoint: Endpoint {
    typealias ResponseType = MyModel

    let baseURL: URL = URL(string: "https://mysite.com/api")!
    let path: String = "/mymodel/1"
    let method: HTTPMethod = .get

    func unpack(data: Data) throws -> MyModel {
        let decoder = JSONDecoder.myDecoder
        return try decoder.decode(MyModel.self, from: data)
    }
}

常用扩展

在许多情况下,你希望为所有端点使用单个 baseURL,你可以通过扩展 Endpoint 协议来实现。

extension Endpoint {
    var baseURL: URL {
        return URL(string: "https://mysite.com/api")!
    }
}

传输器 (Transporters)

Communicator 类依赖于 Transporter 协议来完成繁重的工作。这是一个包含单个函数的简单协议。

public protocol Transporter {
    func send(
        _ request: URLRequest, 
        completionHandler: @escaping (Result<TransportationResult, CommunicatorError>) -> Void
    ) -> Request
}

默认情况下,Endpoints 扩展了 URLSession 以符合此协议,然后使用它来执行实际的网络请求。 这允许你创建自己的自定义 Transporter,例如用于身份验证。

class AuthorizationTransporter: Transporter {
    private let base: Transporter
    private let authenticationDetails: String

    init(base: Transporter, authenticationDetails: String) {
        self.base = base
        self.authenticationDetails = authenticationDetails
    }

    func send(
        _ request: URLRequest, 
        completionHandler: @escaping (Result<TransportationResult, CommunicatorError>) -> Void
    ) -> Request {
        var modifiedRequest = request
        modifiedRequest.addValue("Basic \(authenticationDetails)", forHTTPHeaderField: "Authorization")

        return base.send(modifiedRequest, completionHandler: completionHandler)
    }
}

let authTransporter = AuthorizationTransporter(base: URLSession.shared, authenticationDetails: "...")
let communicator = Communicator(transporter: authTransporter)

测试

你可能需要测试基于 Endpoints 构建的代码。 为了帮助你,Endpoints 包含了 EndpointsTesting 包,它在测试时为你提供有用的类。 下面是一个示例代码片段,展示了它的用法。

import EndpointsTesting

class MyTestCase: XCTestCase {
    func testMyCode() {
        // The TestTransporter class can enqueue responses, that are responded with in FIFO-order
        // (first-in first-out). This allows you to set up a chain of responses.
        let testTransporter = TestTransporter(responses: [
            .success(.init(code: 200, data: MyTestFixture.sampleData))
        ])

        // When setting up the Communicator, simply pass in the TestTransporter
        let communicator = Communicator(transporter: testTransporter)

        // Done! Here you would typically put your test code
    }
}

安装

要开始使用 Endpoints,只需将其添加为 Swift Package 依赖项。

.package(url: "https://github.com/isotopsweden/Endpoints", from: "3.0.0")