CombineRequest

CombineRequest 是一个灵活的框架,用于构建与 API 通信的一系列请求。

安装

通过 Swift Package Manager 进行安装。 将此仓库的 URL 粘贴到 Xcode 中,或将此行添加到您的 Package.swift 文件中。

.package(url: "https://github.com/lightyear/CombineRequest", from: "1.0.0")

使用

此软件包提供了两种主要类型:RequestAPIBase

Request 是一个协议,描述了 API 请求的基本要素。 它定义了 HTTP 方法、端点路径、您期望接收的数据类型以及任何可能的错误(通常只是 Error)。 从 JSONPlaceholder 获取用户的示例代码如下所示:

class UsersRequest: APIBase, Request {
    override init() {
        super.init()
        path = "https://jsonplaceholder.typicode.com/users"
    }
    
    func start() -> AnyPublisher<Data, Error> {
        super.sendRequest()
            .map { $0.data }
            .eraseToAnyPublisher()
    }
}

cancellable = UsersRequest()
    .start()
    .catch {
        // error handling
    }
    .sink {
        // $0 is a Data instance with the response JSON
    }

APIBase 是另一种类型。 它包含一个 URLSession 实例,构建 URLRequest 并启动数据任务。 它旨在被子类化,并包含给定 API 的所有请求的通用逻辑。 同样,对于 JSONPlaceholder,子类可能如下所示:

class JSONPlaceholderAPI: APIBase {
    override init() {
        super.init()
        baseURL = URL(string: "https://jsonplaceholder.typicode.com")
    }
    
    override func buildURLRequest() -> URLRequest? {
        var urlRequest = super.buildURLRequest()
        urlRequest?.setValue("application/json", forHTTPHeaderField: "Accept")
        return urlRequest
    }
    
    override func startRequest() -> AnyPublisher<DataResponseTuple, Error> {
        super.startRequest()
            .validateStatusCode(in: 200..<300)
            .hasContentType("application/json")
            .eraseToAnyPublisher()
    }
}

此子类确保为每个请求设置 Accept 标头,并验证响应的 HTTP 状态代码和内容类型。 请注意,只有叶子类才符合 Request。 这一点很重要,因为 Swift 不会进一步向下查找继承层次结构以找到属性或函数的正确实现。

解码 JSON 数据

从请求返回 Data blob 不如结构化数据有用。 可以稍微修改 UsersRequest 以自动执行此操作

struct User: Codable {
    var id: Int
    var name: String
    var username: String
    var email: String
    // etc...
}

class UsersRequest: JSONPlaceholderAPI, Request {
    override init() {
        super.init()
        path = "/users"
    }

    func start() -> AnyPublisher<[User], Error> {
        super.sendRequest()
            .decode(type: [User].self, decoder: JSONDecoder())
            .eraseToAnyPublisher()
    }
}

start() 的返回类型已更改以反映解码后的类型,并且 decode 运算符用于将 Data 解析为 Array<User>

自定义运算符

有几个有用的运算符可用于验证响应数据是否与您期望的匹配。

如果响应状态代码不是提供的序列,则 validateStatusCode(in:) 会生成一个错误,从而导致管道失败。 您可以传递任何 IntSequence(因此,Range<Int>Set<Int>Array<Int> 都可以)。

如果响应内容类型与传递的类型不匹配,则 hasContentType(_:) 会生成一个错误。 此运算符将匹配带或不带尾随字符集的内容类型。 例如,hasContentType("text/plain") 接受 "text/plain"(完全匹配)或 "text/plain; charset=utf-8" 的内容类型。

测试

您可以使用任何连接到 Apple URL 加载系统的库来测试您的 Request 一致性,例如 OHHTTPStubs

另一种选择是利用 Combine。 APIBase 公开了 dataTaskPublisher 属性,该属性通常在您的代码调用 sendRequest() 时延迟创建。 如果您将自己的 publisher 分配给此属性,则可以使 URL 加载系统短路并立即生成响应或错误。 APIBase 中有一些辅助函数

stub(with: HTTPURLResponse, data: Data)

创建一个 publisher,该 publisher 生成一个 (data, response) 元组并完成。 此 stub 使您可以最灵活地构建您的代码期望的响应。

stubResponse(statusCode: Int, data: Data, headers: [String: String])
stubJSONResponse(statusCode: Int, data: Data, headers: [String: String])

创建一个 publisher,该 publisher 生成一个 (data, response) 元组并完成。 这两个 stub 负责处理一些样板代码:将正确的 URL 放入响应中,包括 Content-Length 和(对于 JSON 版本)Content-Type 标头。

stub(error: Error)

创建一个 publisher,该 publisher 立即因提供的错误而失败。 如果您想测试网络级别的故障(没有 Internet 连接、DNS 故障等),那么这就是您想要的 stub。 如果您想测试 4xx 和 5xx HTTP 故障,从网络的角度来看,这些实际上是非错误情况,因此您将使用上面的其中一个 stub 并带有适当的状态代码。

您可以在此存储库的测试套件中看到两种测试方法的示例。