端点
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
session.start(call: call) { result in
result.onSuccess { value in
//value is an object of the type specified in `Call`
}.onError { error in
//something went wrong
}
}
响应解析
一个调用应当确切知道其请求应该得到什么响应。它会将响应解析委托给一个 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"]))
...
session.start(call: call) { result in
result.onSuccess { value in
//value is now a JSON dictionary 🎉
}
}
let call = AnyCall<JSONParser<GiphyGif>>(Request(.get, "gifs/random", query: ["tag": "cat", "api_key": "dc6zaTOxFJmzC"]))
...
session.start(call: call) { result in
result.onSuccess { value in
//value is now a `GiphyGif` dictionary 🎉
}
}
ResponseParser
缓存
提供的 查阅代码中的文档,以获取类型的进一步解释。
DataResponseParser
DictionaryParser
JSONParser
NoContentParser
StringConvertibleParser
StringParser
JSON Codable 集成
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"
func encode<C: Call>(call: C) -> URLRequest {
var request = anyClient.encode(call: call)
// Append the API key to every request
request.append(query: ["api_key": apiKey])
return request
}
public func parse<C : Call>(sessionTaskResult result: URLSessionTaskResult, for call: C) throws -> C.Parser.OutputType {
do {
// Use `AnyClient` to parse the response
// If this fails, try to read error details from response body
return try anyClient.parse(sessionTaskResult: result, for: call)
} catch {
// See if the backend sent detailed error information
guard
let response = result.httpResponse,
let data = result.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)
session.start(call: call) { result in
result.onSuccess { value in
print("image url: \(value.data.url)")
}.onError { error in
print("error: \(error)")
}
}
便利性
有多种方法可以使得执行调用更方便。您可以编写一个专门的GiphyCall
,为用户创建正确的Client
和Session
。
protocol GiphyCall: Call {}
extension GiphyCall {
func start(completion: @escaping (Result<Parser.OutputType>)->()) {
let client = GiphyClient()
let session = Session(with: client)
session.start(call: self, completion: completion)
}
}
当GiphyCall
被GetRandomImage
采用,而不是使用Call
,执行请求将更加简单。
GetRandomImage(tag: "cat").start { result in ... }
为了更容易地找到支持的调用,您可以使用扩展您的Client
来对调用进行命名空间。
extension GiphyClient {
struct GetRandomImage: GiphyCall { ... }
}
Xcode现在可以帮助开发者在Xcode中找到正确的Call
实例。
GiphyClient.GetRandomImage(tag: "cat").start { result in ... }
安装
CocoaPods
pod "Endpoints"
Carthage
github "tailoredmedia/Endpoints.git"
Swift 包管理器
.package(url: "https://github.com/tailoredmedia/Endpoints.git", .upToNextMajor(from: "2.0.0"))
示例
要编译示例,您需要在 Xcode 中打开项目,依赖项通过 Swift 包管理器添加,Xcode 会自动下载它们。
要求
- Swift 5
- iOS 8
- tvOS 9
- macOS 10.11
- watchOS 2.0