Retrolux 0.10.4

Retrolux 0.10.4

测试已测试
语言语言 SwiftSwift
许可证 MIT
发布上次发布2017年10月
SwiftSwift 版本3.1
SPM支持 SPM

Bryan Henderson 维护。



Retrolux 0.10.4

  • 编者:
  • Bryan Henderson

Retrolux 是一个一站式网络框架。它旨在成为 Android 的等效 Square's Retrofit

本框架还处于初期开发阶段。

为什么?

目前已经有许多优秀的 iOS 网络库。例如 AlamofireAFNetworkingMoya 等都是优秀的库。

此框架的独特之处在于每个端点都可以简洁地描述。无需子类化、实现协议、实现函数或下载额外的模块。它包含 JSON、表单数据和 URL 编码支持。简而言之,它旨在尽可能优化网络 API 消费的全过程。

此框架的目的不仅是抽象网络细节,还提供了类似 Retrofit 的工作流程,其中可以描述端点而不是实现。

安装

实例

JSON

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 类提供。

可以通过向构建器发送 FieldPart 对象来发送多部分数据。


简单的登录

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 编码

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)

更改基础 URL

请求在调用 .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