端点 (Endpoints) 可以轻松地为任何 Web-API 编写类型安全的网络抽象层。
它需要 Swift 5,大量使用泛型(和广义存在类型)以及协议(和协议扩展)。它还鼓励清晰的关注点分离和值类型的使用(例如结构体)。
以下是如何从 Giphy 加载随机图片的方法。
// A client is responsible for encoding and parsing all calls for a given Web-API.
let client = AnyClient(baseURL: URL(string: "https://api.giphy.com/v1/")!)
// A call encapsulates the request that is sent to the server and the type that is expected in the response.
let call = AnyCall<DataResponseParser>(Request(.get, "gifs/random", query: ["tag": "cat", "api_key": "dc6zaTOxFJmzC"]))
// A session wraps `URLSession` and allows you to start the request for the call and get the parsed response object (or an error) in a completion block.
let session = Session(with: client)
// enable debug-mode to log network traffic
session.debug = true
// start call
let (body, httpResponse) = try await session.dataTask(for: call)
一个调用应该清楚地知道从请求中期望什么样的响应。它将响应的解析委托给一个 ResponseParser
。
一些内置类型已经采用了 ResponseParser
协议(使用协议扩展),因此例如,您可以将任何响应转换为 JSON 数组或字典。
// Replace `DataResponseParser` with any `ResponseParser` implementation
let call = AnyCall<DictionaryParser<String, Any>>(Request(.get, "gifs/random", query: ["tag": "cat", "api_key": "dc6zaTOxFJmzC"]))
...
// body is now a JSON dictionary 🎉
let (body, httpResponse) = try await session.dataTask(for: call)
let call = AnyCall<JSONParser<GiphyGif>>(Request(.get, "gifs/random", query: ["tag": "cat", "api_key": "dc6zaTOxFJmzC"]))
...
// body is now a `GiphyGif` dictionary 🎉
let (body, httpResponse) = try await session.dataTask(for: call)
请查阅代码中的文档以获取有关这些类型的更多解释。
DataResponseParser
DictionaryParser
JSONParser
NoContentParser
StringConvertibleParser
StringParser
Endpoints
内置了 JSON Codable 支持。
负责处理可解码类型的 ResponseParser
是 JSONParser
。JSONParser
使用默认的 JSONDecoder()
,但是,JSONParser
可以被子类化,并且可以使用您配置的 JSONDecoder
覆盖 jsonDecoder
。
// Decode a type using the default decoder
struct GiphyCall: Call {
typealias Parser = JSONParser<GiphyGif>
...
}
// custom decoder
struct GiphyParser<T>: JSONParser<T> {
override public var jsonDecoder: JSONDecoder {
let decoder = JSONDecoder()
// configure...
return decoder
}
}
struct GiphyCall: Call {
typealias Parser = GiphyParser<GiphyGif>
...
}
每个可编码的对象都能够提供一个 JSONEncoder()
,通过 toJSON()
方法对其进行编码。
AnyCall
是 Call
协议的默认实现,您可以直接使用。但是,如果您想使您的网络层真正具有类型安全性,您需要为 Web-API 的每个操作创建一个专用的 Call
类型。
struct GetRandomImage: Call {
typealias Parser = DictionaryParser<String, Any>
var tag: String
var request: URLRequestEncodable {
return Request(.get, "gifs/random", query: [ "tag": tag, "api_key": "dc6zaTOxFJmzC" ])
}
}
// `GetRandomImage` is much safer and easier to use than `AnyCall`
let call = GetRandomImage(tag: "cat")
客户端负责处理给定 Web-API 的所有操作的通用事项。通常包括将 API 令牌或身份验证令牌附加到请求,或验证响应并处理错误。
AnyClient
是 Client
协议的默认实现,可以直接使用,也可以作为您自己的专用客户端的起点。
您通常需要创建自己的专用客户端,该客户端要么是 AnyClient
的子类,要么将请求的编码和响应的解析委托给 AnyClient
实例,就像这里所做的那样。
class GiphyClient: Client {
private let anyClient = AnyClient(baseURL: URL(string: "https://api.giphy.com/v1/")!)
var apiKey = "dc6zaTOxFJmzC"
override func encode<C>(call: C) async throws -> URLRequest {
var request = anyClient.encode(call: call)
// Append the API key to every request
request.append(query: ["api_key": apiKey])
return request
}
override func parse<C>(response: HTTPURLResponse?, data: Data?, for call: C) async throws -> C.Parser.OutputType
where C: Call {
do {
// Use `AnyClient` to parse the response
// If this fails, try to read error details from response body
return try await anyClient.parse(sessionTaskResult: result, for: call)
} catch {
// See if the backend sent detailed error information
guard
let response,
let data,
let errorDict = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any],
let meta = errorDict?["meta"] as? [String: Any],
let errorCode = meta["error_code"] as? String
else {
// no error info from backend -> rethrow default error
throw error
}
// Propagate error that contains errorCode as reason from backend
throw StatusCodeError.unacceptable(code: response.statusCode, reason: errorCode)
}
}
}
通常,您希望您的网络层为每个支持的调用提供专用的响应类型。在我们的示例中,它可能看起来像这样
struct RandomImage: Decodable {
struct Data: Decodable {
let url: URL
private enum CodingKeys: String, CodingKey {
case url = "image_url"
}
}
let data: Data
}
struct GetRandomImage: Call {
typealias Parser = JSONParser<RandomImage>
...
}
有了所有的部分,您的网络层的用户现在可以执行类型安全的请求,并通过几行代码获得类型安全的响应
let client = GiphyClient()
let call = GetRandomImage(tag: "cat")
let session = Session(with: client)
let (body, response) = try await session.dataTask(for: call)
print("image url: \(body.data.url)")
Swift 包管理器
.package(url: "https://github.com/tailoredmedia/Endpoints.git", .upToNextMajor(from: "3.0.0"))
示例实现可以在这里找到。