HJRestClientManager
基于 Hydra 框架的简单灵活的 REST API 客户端。
安装
您可以从我们的发行页面下载最新的框架文件。HJRestClientManager 也可通过 CocoaPods 获得。要安装它,只需将以下行添加到 Podfile 中。
pod 'HJRestClientManager'
简单预览
这是调用通过 GET 方法请求 'http://your.apiserver.com/hello?a=1' 的 API 的示例代码。
HJRestClientManager.request().serverAddress("your.apiserver.com/hello").requestModel(["a":"1"]).resume() { (result:[String: Any]?) -> [String: Any]? in
if let data = result?[HJRestClientManager.NotificationResponseModel] as? Data, let bodyString = String(data: data, encoding: .utf8) {
print(bodyString)
}
return nil
}
特性
- 通过协议独裁概念轻松管理多个协议。
- 通过串行或并发管理多个请求方法组。
- 支持链式请求/响应方法。
- 支持通过远程请求管理自定义本地工作。
- 支持管理连接池。
- 支持管理本地缓存。
- 支持管理反应式共享数据机制。
- 支持模拟测试环境。
设置
HJRestClientManager 是基于 Hydra 的框架。并且,它也使用了另一个基于 Hydra 的库 HJResourceManager。首先,按照你的目的向 Hydra 添加工作者。在这种情况下,只需要创建两个工作者并为每个管理器使用。现在,绑定并启动 HJResourceManager 和 HJRestClientManager,使两者相互绑定。
Hydra.default().addNormalWorker(forName: "hjrm")
Hydra.default().addNormalWorker(forName: "hjrc")
let hjrmRepoPath = "\(NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0])/hjrm"
if HJResourceManager.default().standby(withRepositoryPath: hjrmRepoPath, localJobWorkerName: "hjrm", remoteJobWorkerName: "hjrm") {
HJResourceManager.default().bind(toHydra: Hydra.default())
}
let hjrcRepoPath = "\(NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0])/hjrc"
if HJRestClientManager.shared.standby(withRepositoryPath: hjrcRepoPath, workerName: "hjrc") == true {
HJRestClientManager.shared.bind(toHydra: Hydra.default())
}
Hydra.default().startAllWorkers()
实施 Dogma
现在,您可以调用 REST API 并处理响应,就像您在简短预览中所看到的那样。HJRestClientManager 具有协议 dogma 概念。它可以更优雅地处理协议。您可以使用 HJRestClientDogma 定义并实现业务代码以用于 API 协议的请求/响应模型。以下是 HJRestClientDogma 协议。
// HJRestClientDogma protocol
@objc public protocol HJRestClientDogma {
@objc func bodyFromRequestModel(_ requestModel:Any?) -> Data?
@objc func contentTypeFromRequestModel(_ requestModel:Any?) -> String?
@objc func responseModel(fromData data:Data, serverAddress:String, endpoint:String?, contentType:String?, requestModel:Any?, responseModelRefer:Any?) -> Any?
@objc func responseData(fromModel model:Any, serverAddress:String, endpoint:String?, requestModel:Any?) -> Data?
@objc func didReceiveResponse(response:URLResponse, serverAddress:String, endpoint:String?)
}
// Your dogma class
class SampleDogma: HJRestClientDogam {
// implement here.
}
让我们假设您的 REST API 使用 json 格式作为 post 方法的数据体,并且响应体格式也是 json。在这个例子中,我们使用 ObjectMapper 库作为响应模型映射器。
让我们逐一查看。
bodyFromRequestModel 函数接收您的自定义请求模型对象,您需要返回它的体数据。
func bodyFromRequestModel(_ requestModel:Any?) -> Data? {
if let jsonModel = requestModel as? Mappable, let jsonString = jsonModel.toJSONString() {
return jsonString.data(using: .utf8)
}
return requestModel as? Data
}
contentTypeFromRequestModel 函数接收您的自定义请求模型对象,您需要返回它的 Content-Type。
func contentTypeFromRequestModel(_ requestModel:Any?) -> String? {
if (requestModel as? Mappable) != nil {
return "application/json"
}
return "application/octet-stream"
}
responseModel 函数接收从服务器来的数据,以及一些关于服务的额外信息、数据的内容类型、当它调用时请求模型对象和所需的响应模型类型。在处理它之后,您需要返回响应模型对象。
func responseModel(fromData data: Data, serverAddress: String, endpoint: String?, contentType:String?, requestModel: Any?, responseModelRefer: Any?) -> Any? {
guard let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []) else {
return nil
}
if let list = jsonObject as? [Any] {
var modelList:[Any] = []
for item in list {
if let item = item as? [String:Any], let jsonModelClass = responseModelRefer as? Mappable.Type, let model = try? jsonModelClass.init(dictionary: item) {
modelList.append(model)
}
}
return modelList
}
if let item = item as? [String:Any], let jsonModelClass = responseModelRefer as? Mappable.Type, let model = try? jsonModelClass.init(dictionary: item) {
return model;
}
return nil
}
responseData 函数接收您的自定义响应模型对象、一些关于服务的额外信息和当它调用时请求模型对象。您需要将响应模型的数据对象返回到本地缓存。
func responseData(fromModel model: Any, serverAddress: String, endpoint: String?, requestModel: Any?) -> Data? {
if let jsonModel = model as? Mappable, let jsonString = jsonModel.toJSONString() {
return jsonString.data(using: .utf8)
}
return model as? Data
}
didRecieveResponse 函数接收响应对象和一些有关服务器的额外信息。每当您收到某些东西时,它都会被调用,因此您需要检查响应对象,然后在这里进行处理。
func didReceiveResponse(response: URLResponse, serverAddress: String, endpoint: String?) {
if let urlResponse = response as? HTTPURLResponse {
print(urlResponse.allHeaderFields)
}
}
在定义和实现了您的 dogma 之后,您需要在使用之前使用键对其进行注册。
HJRestClientManager.shared.setDogma(SampleDogma(), forKey: "sampleDogma")
现在您可以使用您的 dogma 调用您的 REST API。以下是通过 GET 方法调用 'http://postman-echo.com/get?a=1' 的示例代码。
// Your request model class
class Req: Mappable {
// ...
}
// Your response model class
class Res: Mappable {
// ...
}
let req = Req(JSONString: "{\"name\":\"gandalf\"}")
HJRestClientManager.request().dogmaKey("sampleDogma").serverAddress("your.apiserver.com/hello").post().requestModel(req).responseModelRefer: Res.self).resume() { (result:[String: Any]?) -> [String: Any]? in
if let model = result?[HJRestClientManager.NotificationResponseModel] as? Res {
print("gotta")
}
return nil
}
您可以定义和实现多个 dogma 并简单使用它。
HJRestClientManager.request().dogmaKey("sampleDogma").serverAddress("your.apiserver.com/v1/hello").post().req...
HJRestClientManager.request().dogmaKey("anotherDogma").serverAddress("your.apiserver.com/v2/hello").post().req...
您可以将默认 dogma 设置为 HJRestClientManager,这样可以减少您的输入。如果有请求没有其 dogma 键,则 HJRestClientManager 将使用默认 dogma 键。
HJRestClientManager.shared.defaultDogmaKey = "sampleDogma"
HJRestClientManager.request().serverAddress("your.apiserver.com/hello").post().requestModel(req).responseModelRefer: Res.self).resume() { (result:[String: Any]?) -> [String: Any]? in
if let model = result?[HJRestClientManager.NotificationResponseModel] as? Res {
print("gotta")
}
return nil
}
管理服务器地址、端点和路径组件
我们通常会将服务器地址和API端点分开管理。您可以通过按地址划分来描述端点来实现这一点。
HJRestClientManager.request().serverAddress("your.apiserver.com").endpoint("/v1/hello").post().requestModel(req).responseModelRefer: Res.self).resume() { (result:[String: Any]?) -> [String: Any]? in
if let model = result?[HJRestClientManager.NotificationResponseModel] as? Res {
print("gotta")
}
return nil
}
服务器地址也可以通过HJRestClientManager中的键来管理,您可以通过键简单描述服务器地址。
HJRestClientManager.shared.setServerAddresses( ["apisvr": "your.server.com"] )
HJRestClientManager.request().serverKey("apisvr").endpoint("/v1/hello").post().requestModel(req).responseModelRefer: Res.self).resume() { (result:[String: Any]?) -> [String: Any]? in
if let model = result?[HJRestClientManager.NotificationResponseModel] as? Res {
print("gotta")
}
return nil
}
默认服务器键也可以设置。如果某些请求没有其服务器地址信息,则HJRestClientManager将使用默认服务器键。
HJRestClientManager.shared.defaultServerKey = "apisvr"
HJRestClientManager.request().endpoint("/v1/hello").post().requestModel(req).responseModelRefer: Res.self).resume() { (result:[String: Any]?) -> [String: Any]? in
if let model = result?[HJRestClientManager.NotificationResponseModel] as? Res {
print("gotta")
}
return nil
}
您还可以为服务器地址键和端点创建别名以对应API键。因此,所有这些,您可以这样调用:
HJRestClientManager.shared.setApiWith(serverKey: "apisvr", endpoint: "/v1/hello", forKey: "hello")
HJRestClientManager.request().apiKey("hello").post().requestModel(req).responseModelRefer: Res.self).resume() { (result:[String: Any]?) -> [String: Any]? in
if let model = result?[HJRestClientManager.NotificationResponseModel] as? Res {
print("gotta")
}
return nil
}
有时,您可能希望用请求信息替换某些端点的路径组件。例如,API /api/hello 允许昵称和消息作为最后两个组件。
http://your.apiserver.com/api/hello/ <nickname> / <message>
您可以在调用请求之前创建URL字符串,或者保留替换组件并按如下方式传递参数。下面的键$0和$1不是保留词,因此您可以使用任何您想要的单词。
setReplaceComponents函数接收包含查找键值对以及默认值的键/值集合,如果请求参数和端点格式未接收替换值,则可以使用默认值。
HJRestClientManager.shared.setReplaceComponents(["$0":"", "$1":""], forEndpoint: "/api/hello/$0/$1")
HJRestClientManager.request().endpoint("/api/hello/$0/$1").post().requestModel(["$0":"gandalf"]).responseModelRefer: Res.self).resume() { (result:[String: Any]?) -> [String: Any]? in
if let model = result?[HJRestClientManager.NotificationResponseModel] as? Res {
print("gotta")
}
return nil
}
如果请求参数中存在给定键值,则将替换其值;如果不存在,则将替换给定的默认值。因此,上面的代码请求端点将变成"/api/hello/gandalf"。
同时进行多个请求并等待全部完成。
在进行多个请求之前,您需要为请求节点和其业务代码提供响应(如果需要的话)。
let reqA = HJRestClientManager.RequestNode().apiKey("hello").requestModel(["$0":"gandalf"]).responseModelRefer(Res.self)
let reqB = HJRestClientManager.RequestNode().apiKey("hello").requestModel(["$0":"balrog"]).responseModelRefer(Res.self).completionHandler { (result:[String : Any]?) -> [String : Any]? in
if let model = result?[HJRestClientManager.NotificationResponseModel] as? Res {
// here will be placed some business code if you want to handle the response additionally or doing something indpendently without combined response handler.
}
return nil
}
HJRestClientManager.shared.requestConcurrent([reqA, reqB], callIdentifier: nil) { (result:[String : Any]?) -> [String : Any]? in
guard let result = result, let eventValue = result[HJRestClientManager.NotificationEvent] as? Int, let event = HJRestClientManager.Event(rawValue: eventValue) else {
return nil
}
switch event {
case .doneRequestGroup :
let stopped = result[HJRestClientManager.NotificationRequestGroupStopped] as? Bool ?? false
if stopped == true {
print("request group stopped")
}
if let results = result[HJRestClientManager.NotificationRequestGroupResults] as? [[String:Any]] {
for res in results {
if res[HJRestClientManager.NotificationResponseModel] == nil {
print("request have no response.")
}
}
print("all group req done.")
} else {
print("something wrong in group!")
}
default :
break
}
return nil
}
链式请求
在进行链式请求之前,您需要制作与多个请求相同的请求节点。然后通过使用类似requestConcurrent的做法调用requestSerial函数。然后,HJRestClientManager将逐个、逐步地请求它。但链式请求的重点是使用前一个请求的响应数据作为下一个请求。您可以通过requestModelFromResultHandler函数来进行描述。
let reqA = HJRestClientManager.RequestNode().apiKey("hello").requestModel(["$0":"gandalf"]).responseModelRefer(Res.self)
let reqB = HJRestClientManager.RequestNode().apiKey("hello").responseModelRefer(Res.self).requestModelFromResultHandler { (result:[String : Any]?) -> Any? in
if let model = result?[HJRestClientManager.NotificationResponseModel] as? Res {
let reqm = ["$0":"\(Res.name)"]
return reqm
}
return nil
}
HJRestClientManager.shared.requestSerial([reqA, reqB], callIdentifier: nil, stopWhenFailed: true) { (result:[String : Any]?) -> [String : Any]? in
guard let result = result, let eventValue = result[HJRestClientManager.NotificationEvent] as? Int, let event = HJRestClientManager.Event(rawValue: eventValue) else {
return nil
}
switch event {
case .doneRequestGroup :
let stopped = result[HJRestClientManager.NotificationRequestGroupStopped] as? Bool ?? false
if stopped == true {
print("request group stopped")
}
if let results = result[HJRestClientManager.NotificationRequestGroupResults] as? [[String:Any]] {
for res in results {
if res[HJRestClientManager.NotificationResponseModel] == nil {
print("request have no response.")
}
}
print("all group req done.")
} else {
print("something wrong in group!")
}
default :
break
}
return nil
}
本地作业
有时,您可能想创建独立的地方业务逻辑。或者,您想要在远程请求之前或之后进行处理。此外,您还想要将其放在远程请求之间。本地作业就是为了这个。
按如下方式制作本地作业请求。
let reqL = HJRestClientManager.RequestNode().localRequest(name: "local job") { (result:[String : Any]?) -> Any? in
// get response model from previous request like this, if you this local job using on serial request processing.
if let model = result?[HJRestClientManager.NotificationResponseModel] as? Res {
// do some processing.
return localJobResult
}
// you can also use local job to doing independantly on concurrent request processing or single request.
// do some processing.
return localJobResult
}
您可以从本地作业参数中获取之前请求的结果模型,并将预处理后的结果传递到下一个作业中,用于序列请求处理。
连接池
默认情况下,HJRestClientManager不限制连接数,它取决于硬件限制。但如果你想手动操作,可以通过这种方式实时更改连接池大小。
HJRestClientManager.shared.connectionPoolSize = 64
本地缓存
你可以通过设置更新缓存标志来自动管理所有远程请求的本地缓存数据。通过全局设置设置缓存的默认活动。 (默认值为false)
HJRestClientManager.shared.useUpdateCacheDefault = false
你还可以根据需要为每个单独的请求设置更新缓存标志。
HJRestClientManager.request().endpoint("/api/hello").requestModel(["name":"gandalf"]).responseModelRefer(Res.self).updateCache(true).resume() { (result:[String: Any]?) -> [String: Any]? in
if let model = result?[HJRestClientManager.NotificationResponseModel] as? Res {
print("gotta")
}
return nil
}
本地缓存数据以其请求信息的关键字存储。以下为请求方法、服务器地址、端点、请求模型。因此,你可以通过提供相同的信息来获取本地缓存数据。
还需要注意的是expireTimeInterval。如果你需要处理本地缓存的过期时间,那么你需要提供数据验证的时间长度值。在给定的时间长度内,即使用户已存在,也不可能获取到在此之前存储的本地缓存数据。如果不设置,则设置为0。如果设置,数据始终可用。
例如,你可以提供1小时的过期时间间隔来获取上述例子中的本地缓存数据。
if let res = HJRestClientManager.shared.cachedResponseModel(method: .get, endpoint: "/api/hello", requestModel: ["name":"gandalf"], responseModelRefer: Res.self, expireTimeInterval: 60*60) as? Res {
print(res)
}
if let res = HJRestClientManager.request().endpoint("/api/hello").requestModel(["name":"gandalf"]).responseModelRefer(Res.self).cachedResponseModel(expireTimeInterval: 0) as? Res {
print(res)
}
你也可以直接通过执行更新和清除操作来管理本地缓存数据。
共享数据
HJRestClientManager中的共享数据是一种永久存储机制,具有可观察性和可加密性。你可以通过关键字简单地获取和设置共享数据。
HJRestClientManager.shared.setShared(data: "hello, world!", forKey: "helloKey")
if let message = HJRestClientManager.shared.sharedData(forKey: "helloKey") {
print(message)
}
如果你通过关键字激活对某些共享数据的观察者,当其数据更改时,你可以获得通知。
// reserve observing
HJRestClientManager.shared.setObserver(forSharedDataKey: "helloKey")
// update shared data at some other place or time passed.
HJRestClientManager.shared.setShared(data: "hello, world!", forKey: "helloKey")
// observing notification and do what you want.
@objc func restClientManagerNotificationHandler(notification:Notification) {
guard let userInfo = notification.userInfo, let eventValue = userInfo[HJRestClientManager.NotificationEvent] as? Int, let event = HJRestClientManager.Event(rawValue: eventValue) else {
return
}
switch event {
case .updateSharedData :
if let sharedDataKey = userInfo[HJRestClientManager.NotificationSharedDataKey] as? String {
print("shared data for key \(sharedDataKey) updated.")
}
break
default :
break
}
}
如果你为共享数据关键字预留加密方法,HJRestClientManager将自动管理存储加密和恢复解密。以下是一个例子,你的关键字为“test”的共享数据将使用gz加密存储。
// register cipher moudle to HJResourceManager somewhere in initial process.
HJResourceManager.default().setCipher(HJResourceCipherGzip(), forName: "gz")
// reserve ecryption method for shared data key that you want.
HJRestClientManager.shared.setCipher(name: "gz", forSharedDataKey: "secretHelloKey")
模拟测试环境
有时,我们需要离线测试环境。协议描述已经准备好,但API服务器尚未准备好,需要测试一些API的部分自定义响应,等等。
首先,你需要设置模拟测试环境。 (默认值为false)
HJRestClientManager.shared.useDummyResponse = true
为服务器地址、端点和方法注册模拟响应处理器。响应处理器将从对该服务器地址、端点和方法的请求调用中获取额外头和请求模型。您只需编写一些业务代码并返回响应数据。
HJRestClientManager.shared.setDummyResponseHanlder({ (extraHeaders:[String : Any]?, requestModel:Any?) -> Data? in
return "{\"name\":\"debugPlayer\"}".data(using: .utf8)
}, forServerAddress: "your.apiserver.com", endpoint: "/api/hello", method: .get, responseDelayTime: 0.8)
注册处理器后,您的请求调用将对该服务器地址、端点和方法的响应数据进行模拟响应处理。
HJRestClientManager.request().serverAddress("your.apiserver.com").endpoint("/api/hello").requestModel(["name":"gandalf"]).resume() { (result:[String: Any]?) -> [String: Any]? in
if let data = result?[HJRestClientManager.NotificationResponseModel] as? Data, let s = String(data: data, encoding: .utf8) {
// this response string is {"name":"debugPlayer"} from dummy response handler.
print(s)
}
return nil
}
如果您的测试完成,只需设置模拟测试环境。您不需要更改任何其他原始业务代码。
HJRestClientManager.shared.useDummyResponse = false
通知监视
您还可以通过观察HJRestClientManager事件来处理业务逻辑。
HJRestClientManager.shared.addObserver(self, selector: #selector(self.restClientManagerNotificationHandler(notification:)))
@objc func restClientManagerNotificationHandler(notification:Notification) {
guard let userInfo = notification.userInfo, let eventValue = userInfo[HJRestClientManager.NotificationEvent] as? Int, let event = HJRestClientManager.Event(rawValue: eventValue) else {
return
}
switch event {
case .loadRemote :
let dogmaKey = userInfo[HJRestClientManager.NotificationDogma] as? String // dogma key
let serverAddress = userInfo[HJRestClientManager.NotificationServerAddress] as? String // server address
let endpoint = userInfo[HJRestClientManager.NotificationEndpoint] as? String // endpoint
let requestModel = userInfo[HJRestClientManager.NotificationRequestModel] // request model as you given model when request call
let callIdentifier = userInfo[HJRestClientManager.NotificationCallIdentifier] as? String // call identifier as you given when request call
let groupIdentifier = userInfo[HJRestClientManager.NotificationGruopIdentifier] as? String // unique group identifier if it belongs to a group request
let statusCode = userInfo[HJRestClientManager.NotificationHttpStatus] as? Int // responsed http status code
let allHeaderFields = userInfo[HJRestClientManager.NotificationHttpHeaders] as? [AnyHashable:Any] // responsed http all headers
let responseModel = userInfo[HJRestClientManager.NotificationResponseModel] // responsed model casted as you given when request call
case .loadSkip, .failInternalError, .failNetworkError, .failServerUnavailable, .failServerUnathorized :
let dogmaKey = userInfo[HJRestClientManager.NotificationDogma] as? String // dogma key
let serverAddress = userInfo[HJRestClientManager.NotificationServerAddress] as? String // server address
let endpoint = userInfo[HJRestClientManager.NotificationEndpoint] as? String // endpoint
let requestModel = userInfo[HJRestClientManager.NotificationRequestModel] // request model as you given model when request call
let callIdentifier = userInfo[HJRestClientManager.NotificationCallIdentifier] as? String // call identifier as you given when request call
let groupIdentifier = userInfo[HJRestClientManager.NotificationGruopIdentifier] as? String // unique group identifier if it belongs to a group request
let statusCode = userInfo[HJRestClientManager.NotificationHttpStatus] as? Int // responsed http status code
let allHeaderFields = userInfo[HJRestClientManager.NotificationHttpHeaders] as? [AnyHashable:Any] // responsed http all headers
case .updateCache :
let dogmaKey = userInfo[HJRestClientManager.NotificationDogma] as? String // dogma key
let serverAddress = userInfo[HJRestClientManager.NotificationServerAddress] as? String // server address
let endpoint = userInfo[HJRestClientManager.NotificationEndpoint] as? String // endpoint
let requestModel = userInfo[HJRestClientManager.NotificationRequestModel] // request model as you given model when request call
let responseModel = userInfo[HJRestClientManager.NotificationResponseModel] // updated response model casted as you given when request call
case .removeCache :
let dogmaKey = userInfo[HJRestClientManager.NotificationDogma] as? String // dogma key
let serverAddress = userInfo[HJRestClientManager.NotificationServerAddress] as? String // server address
let endpoint = userInfo[HJRestClientManager.NotificationEndpoint] as? String // endpoint
let requestModel = userInfo[HJRestClientManager.NotificationRequestModel] // request model as you given model when request call
case .updateSharedData :
let sharedDataKey = userInfo[HJRestClientManager.NotificationSharedDataKey] as? String // shared data key
let sharedData = userInfo[HJRestClientManager.NotificationSharedDataModel] // updated shared data
case .removeSharedData :
let sharedDataKey = userInfo[HJRestClientManager.NotificationSharedDataKey] as? String // shared data key
let sharedData = userInfo[HJRestClientManager.NotificationSharedDataModel] // removed shared data
case .doneReqeustGroup :
let callIdentifier = userInfo[HJRestClientManager.NotificationCallIdentifier] as? String // call identifier as you given when request call
let groupIdentifier = userInfo[HJRestClientManager.NotificationGruopIdentifier] as? String // unique group identifier
let results = userInfo[HJRestClientManager.NotificationRequestGroupResults] as? [[String:Any]] // all result list in group request.
let stopped = userInfo[HJRestClientManager.NotificationRequestGroupStopped] as? Bool // flag to detect all group request done or stopped by some point.
case .custom :
let customEventIdentifier = resultDict[HJRestClientManager.NotificationCustomEventIdentifier] as? String // custom event identifier as you defined
let callIdentifier = userInfo[HJRestClientManager.NotificationCallIdentifier] as? String // call identifier as you given when request call
let groupIdentifier = userInfo[HJRestClientManager.NotificationGruopIdentifier] as? String // unique group identifier if it belongs to a group request
// and any other parameters you setted
default :
break
}
}
许可证
适用于MIT许可证。 http://en.wikipedia.org/wiki/MIT_License