端点 2.2.1

端点 2.2.1

测试已测试
Lang语言 SwiftSwift
许可证 MIT
发布最后发布2022 年 12 月
SPM支持 SPM

Peter WeishaplThomas KollerTailored AppsmariohahnRobin Mayerhofer 维护。



端点 2.2.1

  • 作者:
  • Peter Weishapl、Robin Mayerhofer、Dominik Arnhof 和 Alexander Kauer

CocoaPods Compatible Carthage compatible Swift Package Manager compatible Platform

端点

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
session.start(call: call) { result in
    result.onSuccess { value in
        //value is an object of the type specified in `Call`
    }.onError { error in
        //something went wrong
    }
}

响应解析

一个调用应当确切知道其请求应该得到什么响应。它会将响应解析委托给一个 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"]))
...
session.start(call: call) { result in
    result.onSuccess { value in
        //value is now a JSON dictionary 🎉
    }
}

let call = AnyCall<JSONParser<GiphyGif>>(Request(.get, "gifs/random", query: ["tag": "cat", "api_key": "dc6zaTOxFJmzC"]))
...
session.start(call: call) { result in
    result.onSuccess { value in
        //value is now a `GiphyGif` dictionary 🎉
    }
}

提供的 ResponseParser 缓存

查阅代码中的文档,以获取类型的进一步解释。

  • DataResponseParser
  • DictionaryParser
  • JSONParser
  • NoContentParser
  • StringConvertibleParser
  • StringParser

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() 方法编码自身。

专用调用

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")

专用客户端

客户端负责处理特定Web-API所有操作中的共同事务。通常这包括将API令牌或认证令牌附加到请求,或验证响应和处理错误。

AnyClientClient协议的默认实现,可以直接使用,或作为创建专用客户端的起点。

通常,需要创建自己的专用客户端,这个客户端要么是AnyClient的子类,要么是将请求编码和响应解析委托给AnyClient实例,如同这里的实现。

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

专用响应类型

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

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

类型安全

现在所有的组件都到位了,使用您的网络层的用户可以现在通过几行代码执行类型安全的请求并获得类型安全的响应。

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

session.start(call: call) { result in
    result.onSuccess { value in
        print("image url: \(value.data.url)")
    }.onError { error in
        print("error: \(error)")
    }
}

便利性

有多种方法可以使得执行调用更方便。您可以编写一个专门的GiphyCall,为用户创建正确的ClientSession

protocol GiphyCall: Call {}

extension GiphyCall {
    func start(completion: @escaping (Result<Parser.OutputType>)->()) {
        let client = GiphyClient()
        let session = Session(with: client)
        
        session.start(call: self, completion: completion)
    }
}

GiphyCallGetRandomImage采用,而不是使用Call,执行请求将更加简单。

GetRandomImage(tag: "cat").start { result in ... }

为了更容易地找到支持的调用,您可以使用扩展您的Client来对调用进行命名空间。

extension GiphyClient {
    struct GetRandomImage: GiphyCall { ... }
}

Xcode现在可以帮助开发者在Xcode中找到正确的Call实例。

GiphyClient.GetRandomImage(tag: "cat").start { result in ... }

安装

CocoaPods

pod "Endpoints"

Carthage

github "tailoredmedia/Endpoints.git"

Swift 包管理器

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

示例

要编译示例,您需要在 Xcode 中打开项目,依赖项通过 Swift 包管理器添加,Xcode 会自动下载它们。

要求

  • Swift 5
  • iOS 8
  • tvOS 9
  • macOS 10.11
  • watchOS 2.0