DIAMIR Logo

端点 (Endpoints)

Swift Package Manager compatible

端点 (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
let (body, httpResponse) = try await session.dataTask(for: call)

响应解析

一个调用应该清楚地知道从请求中期望什么样的响应。它将响应的解析委托给一个 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"]))

...

// body is now a JSON dictionary 🎉
let (body, httpResponse) = try await session.dataTask(for: call)
let call = AnyCall<JSONParser<GiphyGif>>(Request(.get, "gifs/random", query: ["tag": "cat", "api_key": "dc6zaTOxFJmzC"]))

...

// body is now a `GiphyGif` dictionary 🎉
let (body, httpResponse) = try await session.dataTask(for: call)

提供的 ResponseParser

请查阅代码中的文档以获取有关这些类型的更多解释。

JSON Codable 集成

Endpoints 内置了 JSON Codable 支持。

解码

负责处理可解码类型的 ResponseParserJSONParser
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() 方法对其进行编码。

专用调用 (Dedicated Calls)

AnyCallCall 协议的默认实现,您可以直接使用。但是,如果您想使您的网络层真正具有类型安全性,您需要为 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")

专用客户端 (Dedicated Clients)

客户端负责处理给定 Web-API 的所有操作的通用事项。通常包括将 API 令牌或身份验证令牌附加到请求,或验证响应并处理错误。

AnyClientClient 协议的默认实现,可以直接使用,也可以作为您自己的专用客户端的起点。

您通常需要创建自己的专用客户端,该客户端要么是 AnyClient 的子类,要么将请求的编码和响应的解析委托给 AnyClient 实例,就像这里所做的那样。

class GiphyClient: Client {
    private let anyClient = AnyClient(baseURL: URL(string: "https://api.giphy.com/v1/")!)
    
    var apiKey = "dc6zaTOxFJmzC"
    
    override func encode<C>(call: C) async throws -> URLRequest {
        var request = anyClient.encode(call: call)
        
        // Append the API key to every request
        request.append(query: ["api_key": apiKey]) 
        
        return request
    }
    
    override func parse<C>(response: HTTPURLResponse?, data: Data?, for call: C) async throws -> C.Parser.OutputType
        where C: Call {
        do {
            // Use `AnyClient` to parse the response
            // If this fails, try to read error details from response body
            return try await anyClient.parse(sessionTaskResult: result, for: call)
        } catch {
            // See if the backend sent detailed error information
            guard
                let response,
                let 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)
        }
    }
}

专用响应类型 (Dedicated Response Types)

通常,您希望您的网络层为每个支持的调用提供专用的响应类型。在我们的示例中,它可能看起来像这样

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>
    ...
}

类型安全 (Type-Safety)

有了所有的部分,您的网络层的用户现在可以执行类型安全的请求,并通过几行代码获得类型安全的响应

let client = GiphyClient()
let call = GetRandomImage(tag: "cat")
let session = Session(with: client)

let (body, response) = try await session.dataTask(for: call)
print("image url: \(body.data.url)")

安装

Swift 包管理器

.package(url: "https://github.com/tailoredmedia/Endpoints.git", .upToNextMajor(from: "3.0.0"))

示例

示例实现可以在这里找到。

要求