Swift 3.2 Platform TravisBuild Coverage codebeat badge Docs @SvenTiigi

PerfectAPIClient 是一个网络抽象层,用于通过 Perfect-CURL 从您的 Perfect 服务器端 Swift 应用程序执行网络请求。它深受 Moya 的启发,并且易于使用且有趣。

安装

要使用 Apple 的 Swift Package Manager 进行集成,请将以下内容作为依赖项添加到您的 Package.swift

.package(url: "https://github.com/SvenTiigi/PerfectAPIClient.git", from: "1.0.0")

这是一个示例 PackageDescription

// swift-tools-version:4.0

import PackageDescription

let package = Package(
    name: "MyPackage",
    products: [
        .library(
            name: "MyPackage",
            targets: ["MyPackage"]
        )
    ],
    dependencies: [
        .package(url: "https://github.com/SvenTiigi/PerfectAPIClient.git", from: "1.0.0")
    ],
    targets: [
        .target(
            name: "MyPackage",
            dependencies: ["PerfectAPIClient"]
        ),
        .testTarget(
            name: "MyPackageTests",
            dependencies: ["MyPackage", "PerfectAPIClient"]
        )
    ]
)

设置

为了使用 PerfectAPIClient 定义网络抽象层,将声明一个枚举来访问 API 端点。在此示例中,我们声明一个 GithubAPIClient 来检索一些 Github zen用户信息

import PerfectAPIClient
import PerfectHTTP
import PerfectCURL
import ObjectMapper

/// Github API Client in order to access Github API Endpoints
enum GithubAPIClient {
    /// Retrieve zen
    case zen
    /// Retrieve user info for given username
    case user(name: String)
    /// Retrieve repositories for user name
    case repositories(userName: String)
}

接下来,我们实现 APIClient 协议来定义请求信息,例如基本 URL、端点路径、HTTP 标头等...

// MARK: APIClient

extension GithubAPIClient: APIClient {
    
    /// The base url
    var baseURL: String {
        return "https://api.github.com/"
    }
    
    /// The path for a specific endpoint
    var path: String {
        switch self {
        case .zen:
            return "zen"
        case .user(name: let name):
            return "users/\(name)"
        case .repositories(userName: let name):
            return "users/\(name)/repos"
        }
    }
    
    /// The http method
    var method: HTTPMethod {
        switch self {
        case .zen:
            return .get
        case .user:
            return .get
        case .repositories:
            return .get
        }
    }
    
    /// The HTTP headers
    var headers: [HTTPRequestHeader.Name: String]? {
        return [.userAgent: "PerfectAPIClient"]
    }
    
    /// The request payload for a POST or PUT request
    var payload: BaseMappable? {
        return nil
    }
    
    /// Advanced CURLRequest options like SSL or Proxy settings
    var options: [CURLRequest.Option]? {
        return nil
    }
    
    /// The mocked result for tests environment
    var mockedResult: APIClientResult<APIClientResponse>? {
        switch self {
        case .zen:
            let request = APIClientRequest(apiClient: self)
            let response = APIClientResponse(
	    	url: self.getRequestURL(), 
		status: .ok, 
		payload: "Some zen for you my friend", 
		request: request
            )
            return .success(response)
        default:
            return nil
        }
    }
    
}

还有一个 JSONPlaceholderAPIClient 示例可用。

用法

PerfectAPIClient 提供了一种访问 API 的简单方法,如下所示:

GithubAPIClient.zen.request { (result: APIClientResult<APIClientResponse>) in
    result.analysis(success: { (response: APIClientResponse) in
        // Do awesome stuff with the response
        print(response.url) // The request url
        print(response.status) // The response HTTP status
        print(response.payload) // The response payload
        print(response.getHTTPHeader(name: .contentType)) // HTTP header field
        print(response.getPayloadJSON) // The payload as JSON/Dictionary
        print(response.getMappablePayload(type: SomethingMappable.self)) // Map payload into an object
        print(response.getMappablePayloadArray(SomethingMappable.self)) // JSON Array
    }, failure: { (error: APIClientError) in
        // Oh boy you are in trouble 😨
    }
}

甚至可以检索一个 JSON 响应作为自动 Mappable 对象。

GithubAPIClient.user(name: "sventiigi").request(mappable: User.self) { (result: APIClientResult<User>) in
    result.analysis(success: { (user: User) in
        // Do awesome stuff with the user
        print(user.name) // Sven Tiigi
    }, failure: { (error: APIClientError) in
        // Oh boy you are in trouble again 😱
    }
}

如果您的响应包含一个 JSON Array

GithubAPIClient.repositories(username: "sventiigi").request(mappable: Repository.self) { (result: APIClientResult<[Repository]>) in
    result.analysis(success: { (repositories: [Repository]) in
        // Do awesome stuff with the repositories
        print(repositories.count)
    }, failure: { (error: APIClientError) in
        // 🙈
    }
}

此示例中的用户对象基于 ObjectMapper 库实现了 Mappable 协议,以执行结构体/类和 JSON 之间的映射。

import ObjectMapper

struct User {
    /// The users full name
    var name: String?
    /// The user type
    var type: String?
}

// MARK: Mappable

extension User: Mappable {
    /// ObjectMapper initializer
    init?(map: Map) {}
    
    /// Mapping
    mutating func mapping(map: Map) {
        self.name   <- map["name"]
        self.type   <- map["type"]
    }
}

错误处理

当您在 APIClientResult 上执行 analysis 函数,或者您在 APIClientResult 上执行简单的 switchif case 操作时,如果发生错误,您将通过 failure 情况检索到 APIClientError。以下示例显示了 APIClientError 上可用的错误类型。

GithubAPIClient.zen.request { (result: APIClientResult<APIClientResponse>) in
    result.analysis(success: { (response: APIClientResponse) in
        // Do awesome stuff with the response
    }, failure: { (error: APIClientError) in
        // Oh boy you are in trouble 😨
	// Analysis the APIClientError
        error.analysis(mappingFailed: { (reason: String, response: APIClientResponse) in
	    // Mapping failed
        }, badResponseStatus: { (response: APIClientResponse) in
            // Bad response status
        }, connectionFailed: { (error: Error, request: APIClientRequest) in
            // Connection failure
        })
    }
}

APIClientError 上的 analysis 函数只是检查检索到哪种错误类型的便捷方式。当然,您可以对 APIClientError 枚举执行 switchif case 操作。

高级用法

修改请求 URL

通过覆盖 modify(requestURL ...) 函数,您可以更新从 baseURL 和 path 构建的请求 URL。当您想每次都将 Token 查询参数添加到您的请求 URL,而不是将其添加到每个路径时,这很方便。

public func modify(requestURL: inout String) {
    requestURL += "?token=42"
}

在映射之前修改 JSON

通过覆盖 modify(responseJSON ...) 函数,您可以在将响应 JSON 从 JSON 映射到您的 mappable 类型之前更新响应 JSON。当响应 JSON 包装在 result 属性中时,这很方便。

public func modify(responseJSON: inout [String: Any], mappable: BaseMappable.Type) {
    // Try to retrieve JSON from result property
    responseJSON = responseJSON["result"] as? [String: Any] ?? responseJSON
}

在映射之前修改 JSON Array

通过覆盖 modify(responseJSONArray ...) 函数,您可以在将响应 JSON Array 映射到可映射的数组之前更新响应 JSON Array。

public func modify(responseJSONArray: inout [[String: Any]], mappable: BaseMappable.Type) {
    // Manipulate the responseJSONArray if you need so
}

应在错误响应状态下失败

通过覆盖 shouldFailOnBadResponseStatus() 函数,您可以决定如果响应状态代码为 >= 300< 200APIClient 是否应将结果评估为失败。默认实现返回 true,这将导致具有错误响应状态代码的响应导致 APIClientResult 的类型为 failure

public func shouldFailOnBadResponseStatus() -> Bool {
    // Default implementation
    return true
}

日志记录

通过重写以下两个函数,您可以在请求开始之前添加日志记录到您的请求,以及当检索到响应时或您可能想要做的其他事情。

将执行请求

通过覆盖 willPerformRequest 函数,您可以在执行 APIClient 的请求之前执行日志记录操作或您可能想要做的其他事情。

func willPerformRequest(request: APIClientRequest) {
    print("Will perform request \(request)")
}

已检索到响应

通过覆盖 didRetrieveResponse 函数,您可以在检索到 APIClient 的请求的响应之后执行日志记录操作或您可能想要做的其他事情。

func didRetrieveResponse(request: APIClientRequest, result: APIClientResult<APIClientResponse>) {
    print("Did retrieve response for request: \(request) and result: \(result)")
}

模拟

为了定义您的 APIClient 处于 UnitIntegration 测试条件下,您需要将 environment 设置为 tests。推荐的方法是覆盖 setUptearDown 并更新 environment,如以下示例所示。

import XCTest
import PerfectAPIClient

class MyAPIClientTestClass: XCTestCase {

    override func setUp() {
        super.setUp()
        // Set to tests environment
        // mockedResult is used if available
        MyAPIClient.environment = .tests
    }
    
    override func tearDown() {
        super.tearDown()
        // Reset to default environment
        MyAPIClient.environment = .default
    }

    func testMyAPIClient() {
    	// Your test logic
    }

}

MockedResult

为了为您的 APIClient 添加模拟以进行单元测试您的应用程序,您可以通过 mockedResult 协议变量返回 APIClientResult。仅当您返回 APIClientResult 并且当前 environment 设置为 tests 时,才会使用 mockedResult

var mockedResult: APIClientResult<APIClientResponse>? {
    switch self {
    case .zen:
        // This result will be used when unit tests are running
        let request = APIClientRequest(apiClient: self)
        let response = APIClientResponse(
		url: self.getRequestURL(), 
		status: .ok, 
		payload: "Keep it logically awesome.", 
		request: request
	)
        return .success(response)
    case .user:
        // A real network request will be performed when unit tests are running
        return nil
    }
}

有关更多详细信息,请查看 PerfectAPIClientTests.swift 文件。

斜杠

当您问自己将斜杠 / 放在哪里时,返回 baseURLpath 的字符串🤔

这是推荐的方式☝️

/// The base url
var baseURL: String {
    return "https://api.awesome.com/"
}
    
/// The path for a specific endpoint
var path: String {
    return "users"
}

在您的 baseURL 的末尾放置一个斜杠,并跳过 path 开头的斜杠。 但是不用担心,APIClient 对于 getRequestURL() 函数有一个默认实现,如果忘记了,它会向 baseURL 添加一个斜杠,如果您的 path 的第一个字符是斜杠,它会删除该字符。 如果您想更改行为,只需覆盖该函数👌

RawRepresentable

由于您的大多数枚举案例将与 关联值 混合,而有些则没有,因此很难将枚举名称作为字符串检索,因为您不能像这样声明具有关联值的枚举

// ❌ Error: enum with raw type cannot have cases with arguments
enum GithubAPIClient: String {
    case zen
    case user(name: String)
}

因此,这是一个通过 RawRepresentable 协议中的 rawValue 属性检索枚举名称的示例

enum GithubAPIClient {
    // Without associated value
    case zen
    // With associated value
    case user(name: String)
}

extension GithubAPIClient: RawRepresentable {
    
    /// Associated type RawValue as String
    typealias RawValue = String
    
    /// RawRepresentable initializer. Which always returns nil
    ///
    /// - Parameters:
    ///   - rawValue: The rawValue
    init?(rawValue: String) {
        // Returning nil to avoid constructing enum with String
        return nil
    }
    
    /// The enumeration name as String
    var rawValue: RawValue {
        // Retrieve label via Mirror for Enum with associcated value
        guard let label = Mirror(reflecting: self).children.first?.label else {
            // Return String describing self enumeration with no asscoiated value
            return String(describing: self)
        }
        // Return label
        return label
    }
    
}

完整示例 GithubAPIClient.swift

用法

print(GithubAPIClient.zen.rawValue) // zen
print(GithubAPIClient.user(name: "sventiigi").rawValue) // user

太棒了😎

Linux 构建说明

确保您已安装 libcurl

sudo apt-get install libcurl4-openssl-dev

如果您在使用 Linux 下的 ObjectMapper 库在 IntDouble 值上遇到 JSON-Mapping 问题,请参阅此 问题

依赖项

PerfectAPIClient 使用以下依赖项

贡献

非常欢迎贡献🙌 🤓

待办事项

许可

MIT License

Copyright (c) 2017 Sven Tiigi

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.