Endpoint 对象描述一个网络 (REST) 端点,并且可以解析它的数据。
Endpoint 对象描述一个网络 API 端点,包括 REST 端点。它包含请求端点数据所需的所有信息,并且可以解析 API 服务器返回的数据。
以这种方式组织端点使代码更具可测试性和组织性,将网络样板代码与端点特定的数据处理分离开来。 EndpointController 管理下载 Endpoint 的数据,以及处理可能发生的错误。
我们构建 Endpoint 库,源于观看了来自 SwiftTalk 团队的精彩视频系列“Tiny Networking Library”。
该库的主要目标是表示一个远程 API 端点。
Endpoint 对象包含向 API 端点发出请求以及如何解析结果所需的一切信息。
分离端点定义和解析功能使得单元测试更加容易。您可以为端点定义参数,并根据已知值检查生成的 URL。 同样,您可以使用已知的测试数据来执行解析例程并验证输出。 单元测试网络代码可能很棘手,但通过将解析代码移动到一个单独的对象中,我们消除了网络方面的复杂性,并大大简化了单元测试。
将端点分解为单独的对象对于我们项目的组织也很有帮助。 趋势是将端点处理放入一个庞大且不断增长的网络控制器中,您需要为每个新端点添加一个方法。 即使是一个简单的项目,您也会很快得到一个难以阅读和维护的巨大的单片控制器。 迁移到端点模型后,一切变得简单。 网络控制器只需要从网络检索数据并处理通信 (HTTP) 错误。 您可以自然地组织端点对象,例如,在其关联的对象类型的扩展中创建工厂方法。
我们还包含一个 EndpointController 对象,用于处理使用 Endpoints 发出网络请求。 EndpointController 使用 URLSession 来检索 Endpoint 数据并检查错误。
Endpoint
类封装了定义网络 API 端点(通常是 REST 端点)的所有数据。 它是关于 Payload
的泛型,Payload
是端点返回的对象类型。 例如,如果您有一个检索有关餐厅信息的端点,则 Payload
类型将是 Restaurant
,例如 Resource<Restaurant>
。
基础 Endpoint
类是一个抽象类。 它定义了一个公共接口和一些默认行为,但有一个空的 parse()
例程。 您应该为特定类型的 PayLoad
对象继承该基类。 该库包含两个子类—— CodableEndpoint 和 FileDownloadEndpoint。
要创建一个 Endpoint,您至少需要 serverUrl
和 pathPrefix
。 serverUrl
是服务的根 URL。 这通常是所有 REST 端点共享的前缀。 pathPrefix
是 URL 路径的下一部分。 如果我们有一个像这样的 URL
http://foo.com/api/v1.0/bar
然后我们可以创建一个 Endpoint 来表示该 URL。
let endpoint = Endpoint<Bar>(serverUrl: URL(string: "http://foo.com/api/v1.0")!,
pathPrefix: "bar")
像许多 REST 服务一样,我们构建的服务器使用一种特定的模式来访问对象。
`http://foo.com/<base_path>/<object_type>/<object_id>/<detail_path>?<query_params>
如果您想要用户 867 的所有关注者,则 URL 将是
`http://foo.com/api/v1.0/users/867/followers?expand=1
Endpoint 初始化程序使用这种模板。 这里的映射是
serverUrl
- http://foo.com/api/v1.0
pathPrefix
- users
objId
- 867
pathSuffix
- followers
queryParams -
["expand": "1"]`所以在代码中
let endpoint = Endpoint<Bar>(serverUrl: URL(string: "http://foo.com/api/v1.0")!,
pathPrefix: "users",
objId: "867",
pathSuffix: "followers",
queryParams: ["expand": "1"])
如果您的服务器不遵循此路径模板,您可以始终只使用基本 URL 作为 serverUrl
,并将路径的其余部分用作 pathPrefix
。
创建端点时的默认 HTTP 方法是 GET
,但 init
的 method
参数允许您指定使用 EndpointHttpMethod
枚举,从 .get
、.post
、.patch
或 .delete
中选择。
Endpoint 支持多种构建 HTTP 请求正文的方法,包括 JSON、表单参数或自定义数据。 任何包含 HTTP 请求正文数据的端点都应使用 .post
或 .patch
HTTP 方法。
要创建 JSON 请求,请在使用 init
方法的 jsonParams
参数创建 Endpoint 时,指定值的字典。 此字典将被 JSON 编码为用作 URLRequest 正文的数据块。
提供 formData
参数将为 URLRequest 创建一个正文,其中提供的字典 url 编码为表单数据。
如果两者都不合适,例如多部分表单数据,您可以使用 init
的 body
属性提供您自己的数据。
dateFormatter
参数保存一个 DateFormatter
对象。 基类不对此做任何事情,但它对于需要 DateFormatter
来解析日期类型的子类很有用,例如 CodableEndpoint
。
此方法返回从 Endpoint 对象的属性生成的 URLRequest。 您可以直接将此请求传递给 URLSession。
对于可分页的端点,您可以指定请求的页面(使用基于“1”的索引)。
该函数还接受要添加到请求的任何额外的标头信息。
open func urlRequest(page: Int = 1, extraHeaders: [String: String] = [:]) -> URLRequest?
返回大量数据的端点通常是分页的。 如果 Payload
类符合 EndpointPageable
协议,则 Endpoint
支持分页。 该协议有一个默认实现,因此一个类只需声明符合性即可启用分页。
extension MyPayloadClass: EndpointPageable {}
这是提供发送到服务器进行分页的参数的默认实现。
public extension EndpointPageable {
static var perPage: Int {
return 30
}
static var perPageLabel: String {
return "per_page"
}
static var pageLabel: String {
return "page"
}
static var pageOffset: Int {
return 0
}
}
page
和 perPageLabel
是发送到服务器的查询参数的名称,分别指示请求的页面和每页的项目数。 perPage
属性是与 perPageLabel
一起发送的数值,用于指定每页数据的项目数。 最后,pageOffset
是在发送到服务器之前对页码的修饰符。 Endpoint
类使用基于 1 的索引作为页面。 如果您的服务器使用基于 0 的索引,您可以将 pageOffset
设置为 -1 以使其匹配。
Endpoint 包包含一个 EndpointController 类,用于处理从 Endpoints 加载数据。 一个应用程序通常只需要一个 EndpointController 实例。 您可以一次又一次地重用此控制器来加载各种 Endpoints
的数据,因此该控制器是一个长期存在的对象。 另一方面,单个 Endpoint
通常是创建、加载然后释放。
EndpointController 构建于 URLSession 之上,有两个主要功能:向服务器发送数据和从服务器接收数据。 控制器使用 Endpoint 创建 URLRequest 并向服务器发出网络请求。 当数据从服务器到达时,控制器使用 Endpoint 的 parse
方法解析数据,或处理交换中发生的任何错误。
除非使用 synchronous
参数明确标记,否则控制器的 load()
方法是异步的。
一旦网络请求开始,load()
调用将立即返回。 请求完成后,主线程执行完成块,如果成功则使用关联的 Payload
对象或如果失败则使用 Error
调用它。
endpointController.load(endpoint) { (result) in
switch result {
case .success(let payload):
print("Success! Received \(payload)")
case .failure(let error):
print("Failure! Error: \(error)")
}
}
EndpointController 的棘手之处在于它需要解释来自服务器的错误。 REST 服务器通常以 JSON 格式返回其错误,但似乎每个服务器都对错误响应 JSON 使用不同的键集。 为了让您(EndpointController 类的使用者)定义错误消息的格式,控制器类是关于 EndpointServerError 协议的泛型。
public protocol EndpointServerError: Codable, Equatable {
var error: String? { get } // Error name -- AuthorizationFailed
var reason: String? { get } // Reson for error -- Username not found
var detail: String? { get } // Further details -- http://www.server.com/error/username.html
}
该协议相对简单。 符合要求的对象需要是 Codable
、Equatable
并且有一些 info 字符串的 getter。 所有三个 getter 都可以返回 nil
,这完全有效,但这意味着从 EndpointController 传回的错误不会携带任何有用的信息。
该包包含一个名为 EndpointDefaultServerError
的示例错误结构体。
public struct EndpointDefaultServerError: EndpointServerError {
public let error: String?
public let reason: String?
public let detail: String?
public init(error: String?, reason: String?, detail: String?) {
self.error = error
self.reason = reason
self.detail = detail
}
public init(reason: String?) {
self.reason = reason
error = nil
detail = nil
}
}
您可以像这样使用此错误结构体实例化一个 EndpointController
let endpointController = EndpointController<EndpointDefaultServerError>()
当发生重要的网络事件时,EndpointController
会发出多个通知以提醒系统中的其他组件。 这些都是 Notification.Name
的实例
endpointServerUnreachable
endpointServerNotResponding
endpointValidationError401Unauthorized
CodableEndpoint
是一个 Endpoint
子类,其中 Payload
必须符合 Codable
。 CodableEndpoint
的 parse()
方法使用 JSONDecoder
将原始数据转换为 Payload
对象。
这是一个使用 CodableEndpoint
检索 OAuth 令牌的示例。
struct Token: Codable {
let type: String
let duration: Int
let value: String
}
let formParms = [
"grant_type": "client_credentials",
"client_id": clientId,
"client_secret": clientSecret
]
let tokenEndpoint = CodableEndpoint<Token>(serverUrl: URL(string: "http://www.foo.com/")!,
pathPrefix: "oauth2/token",
method: .post,
formParams: formParms)
let endpointController = EndpointController<EndpointDefaultServerError>()
endpointController.load(tokenEndpoint) { (result) in
switch result {
case .success(let token):
print("Success! Received token: \(token)")
case .failure(let error):
print("Failure! Error: \(error)")
}
}
FileDownloadEndpoint
是一个 Endpoint
子类,它稍微滥用了 Endpoint
的概念。它不是使用 parse()
方法将数据转换为对象,而是将数据写入本地文件并返回该文件的 URL。初始化与基本 Endpoint 类相似,但增加了一个 destination
参数,指示 parse()
方法应将数据写入何处。parse()
方法将尝试创建 URL 中不存在的任何父目录。parse()
方法完成将文件写入本地磁盘后,destination
URL 将通过完成块返回。由于解析返回的 Payload
是一个 URL
,因此 FileDownloadEndpoint
是从 Endpoint<URL>
继承的。
这是一个使用 FileDownloadEndpoint
下载文件的示例。URL 为 http://www.foo.com/data/foo.csv
的 CSV 文件将被下载到 /tmp/foo.csv
。
let destinationURL = URL(fileURLWithPath: "/tmp/foo.csv")
let csvEndpoint = FileDownloadEndpoint(destination: destinationURL,
serverUrl: URL(string: "http://www.foo.com/")!,
pathPrefix: "data/foo.csv")
let endpointController = EndpointController<EndpointDefaultServerError>()
endpointController.load(csvEndpoint) { (result) in
switch result {
case .success(let url):
print("Success! File written to: \(url)")
case .failure(let error):
print("Failure! Error: \(error)")
}
}
Endpoint
包使用标准的 Swift Logging API。这允许库在不同级别发出日志信息,但由您(库的使用者)来决定日志的目标位置 - 控制台、文件、日志服务等。有关基本设置示例,请参阅 Swift Logging API 项目页面。
Endpoint 可以通过 Swift Package Manager 获得。您可以通过将以下行添加到您的 Package.swift
文件的 dependencies
部分来将 Endpoint 包含到您的项目中
.package(url: "https://github.com/OakCityLabs/Endpoint.git", from: "1.0.5"),
请务必也将其添加到您的 targets
列表中
.target(name: "MyApp", dependencies: ["Endpoint"]),
请参阅 更新日志。
Endpoint 是 Oak City Labs 的产品。