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
上执行简单的 switch
或 if 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
})
}
}
MappingFailed
:表示您的 mappable
类型和响应 JSON
之间的映射不匹配。BadResponseStatus
:表示 APIClient
收到了错误响应状态 >= 300
或 < 200
ConnectionFailed
:表示在对给定 URL 的 CURL 请求期间发生错误。APIClientError
上的 analysis
函数只是检查检索到哪种错误类型的便捷方式。当然,您可以对 APIClientError
枚举执行 switch
或 if case
操作。
通过覆盖 modify(requestURL ...)
函数,您可以更新从 baseURL 和 path 构建的请求 URL。当您想每次都将 Token
查询参数添加到您的请求 URL,而不是将其添加到每个路径时,这很方便。
public func modify(requestURL: inout String) {
requestURL += "?token=42"
}
通过覆盖 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
}
通过覆盖 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
或 < 200
,APIClient
是否应将结果评估为失败。默认实现返回 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
处于 Unit
或 Integration
测试条件下,您需要将 environment
设置为 tests
。推荐的方法是覆盖 setUp
和 tearDown
并更新 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
}
}
为了为您的 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 文件。
当您问自己将斜杠 /
放在哪里时,返回 baseURL
和 path
的字符串🤔
这是推荐的方式☝️
/// 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
的第一个字符是斜杠,它会删除该字符。 如果您想更改行为,只需覆盖该函数👌
由于您的大多数枚举案例将与 关联值 混合,而有些则没有,因此很难将枚举名称作为字符串检索,因为您不能像这样声明具有关联值的枚举
// ❌ 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
}
}
print(GithubAPIClient.zen.rawValue) // zen
print(GithubAPIClient.user(name: "sventiigi").rawValue) // user
太棒了😎
确保您已安装 libcurl
。
sudo apt-get install libcurl4-openssl-dev
如果您在使用 Linux 下的 ObjectMapper 库在 Int
和 Double
值上遇到 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.