测试已测试 | ✓ |
语言语言 | SwiftSwift |
许可证 | MIT |
发布上次发布 | 2017年10月 |
SwiftSwift 版本 | 3.1 |
SPM支持 SPM | ✗ |
由 Bryan Henderson 维护。
Retrolux 是一个一站式网络框架。它旨在成为 Android 的等效 Square's Retrofit。
本框架还处于初期开发阶段。
目前已经有许多优秀的 iOS 网络库。例如 Alamofire、AFNetworking 和 Moya 等都是优秀的库。
此框架的独特之处在于每个端点都可以简洁地描述。无需子类化、实现协议、实现函数或下载额外的模块。它包含 JSON、表单数据和 URL 编码支持。简而言之,它旨在尽可能优化网络 API 消费的全过程。
此框架的目的不仅是抽象网络细节,还提供了类似 Retrofit 的工作流程,其中可以描述端点而不是实现。
Retrolux 的 JSON 支持由 ReflectionJSONSerializer 类提供。
有关反射更多信息,请参阅 Reflectable。
获取项目列表
class Person: Reflection {
var name = ""
var age = 0
}
let builder = Builder(base: URL(string: "https://my.api.com/")!)
let getUsers = builder.makeRequest(
method: .get,
endpoint: "users/",
args: Void, // Or (), because Void == ()
response: [Person].self
)
getUsers().enqueue { response in
switch response.interpreted { // What .interpreted means can be customized by overriding RetroluxBuilder's interpret function
case .success(let users):
print("Got \(users.count) users!")
case .failure(let error):
print("Failed to get users: \(error)")
}
}
提交项目
class Person: Reflection {
var name = ""
var age = 0
}
let builder = Builder(base: URL(string: "https://my.api.com/")!)
let createUser = builder.makeRequest(
method: .post,
endpoint: "users/",
args: Person(),
response: Person.self
)
let newUser = Person()
newUser.name = "Bob"
newUser.age = 3
createUser(newUser).enqueue { response in
print("User created successfully? \(response.isSuccessful)")
if let echo = response.body {
print("Response: \(echo)")
}
}
更新项目
class Person: Reflection {
var id = ""
var name = ""
var age = 0
}
let builder = Builder(base: URL(string: "https://my.api.com/")!)
let patchUser = builder.makeRequest(
method: .patch,
endpoint: "users/{id}/",
args: (Person(), Path("id")),
response: Person.self
)
let existingUser = getExistingUserFromSomewhere()
existingUser.name = "Bob"
existingUser.age = 3
patchUser((newUser, Path(existingUser.id)).enqueue { response in
print("User updated successfully? \(response.isSuccessful)")
if let echo = response.body {
print("Response: \(echo)")
}
}
删除项目
let builder = Builder(base: URL(string: "https://my.api.com/")!)
let deleteUser = builder.makeRequest(
method: .delete,
endpoint: "users/{id}/",
args: Path("id"),
response: Void.self
)
deleteUser(Path(someUser.id)).enqueue { response in
print("User was deleted? \(response.isSuccessful)")
}
多部分支持通过 MultipartFormDataSerializer 类提供。
可以通过向构建器发送 Field 或 Part 对象来发送多部分数据。
简单的登录
class LoginResponse: Reflection {
var token = ""
var user_id = ""
}
let builder = Builder(base: "https://my.api.com/")!)
let login = builder.makeRequest(
method: .post,
endpoint: "login/",
args: (Field("username"), Field("password")),
response: LoginResponse.self
)
login((Field("bobby"), Field("abc123")).enqueue { response in
switch response.interpreted {
case .success(let login):
print("Login successful! Token: \(login.token), user: \(login.user_id)")
case .failure(let error):
print("Failed to login: \(error)")
}
}
图片上传
class User: Reflection {
var id = ""
var name = ""
var image_url: URL?
}
let builder = Builder(base: "https://my.api.com/")!)
let uploadImage = builder.makeRequest(
method: .post,
endpoint: "media_upload/{user_id}/",
args: (Path("user_id"), Part(name: "image", filename: "image.png", mimeType: "image/png")),
response: User.self
)
let image: UIImage = getImageFromCamera()
let imageData = UIImagePNGRepresentation(image)!
uploadImage((Path(someUser.id), Part(imageData)).enqueue { response in
print("image URL is: \(response.body?.image_url)")
}
URL 编码正文由 URLEncodedSerializer 类提供。
默认情况下,多部分序列化器具有更高的优先级。因此,为了使用 URL 编码,您需要在 makeRequest
函数中手动指定您想要的序列化器并将 type: .urlEncoded
添加到其中,如下所示
let login = builder.makeRequest(
type: .urlEncoded, // This is required else the request will be sent as multipart form-data instead!
method: .post,
endpoint: "login/",
args: (Field("username"), Field("password")),
response: LoginResponse.self
)
基本登录示例
class LoginResponse: Reflection {
var id = ""
var token = ""
}
let builder = Builder(base: "https://my.api.com/")!)
let login = builder.makeRequest(
type: .urlEncoded,
method: .post,
endpoint: "login",
args: (Field("username"), Field("password")),
response: LoginResponse.self
)
login((Field("bobby"), Field("abc123")).enqueue { response in
switch response.interpreted {
case .success(let login):
print("id: \(login.id), token: \(login.token)")
case .failure(let error):
print("Login failed: \(error)")
}
}
查询支持由 Query 对象提供。
let find = builder.makeRequest(
method: .get,
endpoint: "users/",
args: (Query("distance"), Query("age_gt")),
response: [User].self
)
find((Query("50"), Query("20")).enqueue { response in
...
}
Retrolux 的许多功能都采用插件的形状。由于其他内置的序列化器也仅是插件,因此添加新序列化器很容易。
例如,假设您想使用 SwiftyJSON 发送/接收 JSON。
import Foundation
import SwiftyJSON
import Retrolux
enum SwiftyJSONSerializerError: Error {
case invalidJSON
}
class SwiftyJSONSerializer: InboundSerializer, OutboundSerializer {
func supports(inboundType: Any.Type) -> Bool {
return inboundType is JSON.Type
}
func supports(outboundType: Any.Type) -> Bool {
return outboundType is JSON.Type
}
func validate(outbound: [BuilderArg]) -> Bool {
return outbound.count == 1 && outbound.first!.type is JSON.Type
}
func apply(arguments: [BuilderArg], to request: inout URLRequest) throws {
let json = arguments.first!.starting as! JSON
request.httpBody = try json.rawData()
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
}
func makeValue<T>(from clientResponse: ClientResponse, type: T.Type) throws -> T {
let result = JSON(data: clientResponse.data ?? Data())
if result.object is NSNull {
throw SwiftyJSONSerializerError.invalidJSON
}
return result as! T
}
}
一旦创建了序列化器,您就可以使用 SwiftyJSON 进行发送/接收。
let builder = Builder(base: URL(string: "https://my.api.com/")!)
// This is how you tell Retrolux to use your serializer.
builder.serializers.append(SwiftyJSONSerializer())
let login = builder.makeRequest(
method: .post,
endpoint: "login/",
args: JSON([:]),
response: JSON.self
)
login(JSON(["username": "bobby", "password": "abc123"])).enqueue { response in
switch response.interpreted {
case .success(let json):
let id = json["id"].stringValue
let token = json["token"].stringValue
print("Got id \(id) and token \(token)")
case .failure(let error):
print("Request failed: \(error)")
}
}
Retrolux 使用“干燥模式”的概念使单元测试更容易。当使用 Builder.dry()
返回的构建器在干燥模式下运行时,所有请求都会跳过 HTTP 客户端,并使用伪造的响应 instead。如果在端点中没有提供伪造的响应,则返回空响应。
要指定在伪造的响应中使用的要求数据,请按照以下方式进行
class LoginArgs: Reflection {
var username = ""
var password = ""
}
class LoginResponse: Reflection {
var id = ""
var token = ""
}
let builder = Builder.dry()
let login = builder.makeRequest(
method: .post,
endpoint: "login/",
args: LoginArgs(),
response: LoginResponse.self,
testProvider: { (creation, starting, request) in
ClientResponse(
url: request.url!,
data: "{\"id\":\"qs492s37\",\"token\":\"0s98q3wj5s5\",\"username\":\"\(starting.username)\"}".data(using: .utf8)!,
status: 200
)
}
)
let args = LoginArgs()
login.username = "bobby"
login.password = "impenetrable"
let response = login(args).perform()
XCTAssert(response.isSuccessful)
XCTAssert(response.body?.id == "qs492s37")
XCTAssert(response.body?.token == "0s98q3wj5s5")
XCTAssert(response.body?.username == args.username)
请求在调用 .enqueue(...)
或 .perform()
时捕获基础 URL。
例如
let builder = Builder(base: URL(string: "https://www.google.com/")!)
let first = builder.makeRequest(
method: .get,
endpoint: "something",
args: (),
Response: Void.self
)
let call = first()
call.enqueue { response in
// response.request.url == "https://www.google.com/something"
}
builder.base = URL(string: "https://www.something.else/")!
call.enqueue { response in
// response.request.url == "https://www.something.else/something"
}
默认启用调试打印语句。要自定义记录,可以像这样继承 Builder 并重写 log
函数
class MyBuilder: Builder {
open override func log(request: URLRequest) {
// To silence logging, do nothing here.
}
open override func log<T>(response: Response<T>) {
// To silence logging, do nothing here.
}
}
反射 API 支持通过实现 static func config(_:PropertyConfig)
函数来自定义行为,如下所示
class MyDateTransformer: NestedTransformer {
enum DateTransformationError: Error {
case invalidDateFormat(got: String, expected: String)
}
typealias TypeOfData = String
typealias TypeOfProperty = Date
let formatter = { () -> DateFormatter in
let f = DateFormatter()
f.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
f.locale = Locale(identifier: "en_US_POSIX")
return f
}()
func setter(_ dataValue: String, type: Any.Type) -> Date {
guard let date = formatter.date(from: value) else {
throw DateTransformationError.invalidDateFormat(
got: value,
expected: formatter.dateFormat
)
}
return date
}
func getter(_ propertyValue: TypeOfProperty) -> String {
return formatter.string(from: value)
}
}
class Person: Reflection {
var desc = "DEFAULT_VALUE"
var notSupported: Int?
var date = Date()
override class func config(_ c: PropertyConfig) {
c["desc"] = [
.serializedName("description"), // Will look for the "description" key in JSON
.nullable // If the value is null, don't raise a "null values not supported" error
]
// 'Int?' is not a supported type, so this will tell Retrolux to ignore it instead of raising an error
c["notSupported"] = [.ignored]
// Alternatively, you can do Reflector.shared.globalTransformers.append(MyDateTransformer())
c["date"] = [.transformed(MyDateTransformer())]
}
}
let reflector = Reflector()
let person = try reflector.convert(
fromDictionary: [
"description": NSNull(),
"date": "2017-04-17T12:02:04.142Z"
],
to: Person.self
) as! Person