大多数浏览器和
Developer App 均支持流媒体播放。
-
认识 Swift OpenAPI Generator
学习如何用 Swift OpenAPI Generator 帮助你使用 HTTP 服务器 API,无论你是在扩展 iOS App 还是在 Swift 中编写服务器。我们将向你展示这个软件包插件如何通过从 OpenAPI 文档生成代码,来简化你的工作流程和代码库。
章节
- 0:44 - Considerations when making API calls
- 1:52 - Meet OpenAPI
- 6:15 - Making API calls from your app
- 12:33 - Adapting as the API evolves
- 14:23 - Testing your app with mocks
- 16:12 - Server development in Swift
- 19:24 - Adding a new operation
资源
- Swift OpenAPI Generator package plugin
- Swift OpenAPI Generator Runtime
- URLSession Transport for Swift OpenAPI Generator
相关视频
WWDC22
-
下载
♪ ♪
Si:大家好 我是 Si 来自 Apple 的 Swift on Server 团队 在本视频中 我将向你展示 Swift OpenAPI Generator 如何帮助你使用服务器 API 无论是用 Swift 扩展 iOS App 还是编写后端服务器 这个新的 Swift 软件包插件 都可以简化你的工作流程 和代码库 今年 我们看到在设备上处理数据 比以往任何时候都更加容易 但有时你想要实现的功能 需要由服务器组件提供动态内容 这意味着需要向远程服务发出 网络请求 调用其 API 但是 为了发出正确的网络请求 需要考虑很多问题 服务器的根 URL 是什么? 构成 API 端点的路径组件有哪些? 应该使用什么 HTTP 方法? 应该如何提供参数? 这些只是调用服务器 API 时 需要考虑的部分问题 对于更复杂的 API 你需要考虑的问题会更多 那么如何回答这些问题呢?
大多数服务都有 某种形式的 API 文档 但是书面的文档 往往会不准确或者过时 尤其是在服务快速发展的情况下 如果你可以访问源代码 你可以查看其执行过程 或者手动测试 API 但这可能会导致你 不能完整理解服务的行为 你可以搜索支持论坛 或依靠其他机构知识以理解其行为 但是 即使是最友善的人 掌握的信息也可能会不足 或不同人提供的答案不一致 从而让你的问题比开始时更多 虽然这些资源 提供了一些帮助 但并不全面 使用更加正式 和结构化的 API 描述 有助于消除歧义 让我们来认识一下 OpenAPI 它是定义 HTTP 服务的开放式规范
OpenAPI 是一个行业标准 它得到了广泛采用 并且发展成熟 这意味着有既定的惯例 和最佳实践来帮助你使用 API 有了 OpenAPI 你可以用 YAML 或 JSON 来记录服务行为 这些机器可读的格式使你能够从 丰富的工具生态系统中获益 这些工具可用于 测试生成、运行时验证 以及互操作性等等 OpenAPI 最为人所熟知的一项特点 就是生成交互式文档的工具 但 OpenAPI 的核心动机是代码生成 它允许采用者使用规范驱动开发 还记得我们举例的 API 端点吗? 在接收到这个请求后 服务器会在一个 JSON 对象中 返回一条个性化的问候语 让我们看看在不使用 代码生成的情况下 调用这个 API 所需要编写的代码
首先 我们需要知道 服务器的根 URL 以便将其转换为组件 然后 我们添加路径组件 来构建 API 端点 并指定参数作为查询项 然后 我们构建一个 URLRequest 并使用 URLSession 进行 HTTP 请求
接下来 我们必须 验证预期类型的响应 具有预期的状态码和内容类型
然后 我们必须解码 响应中的字节 我们通过定义一个 符合 Decodable 的 Swift 类型 并使用 JSONDecoder 来实现这一点 最后 我们从响应中返回消息属性
编写这样的代码很不错 但这只是对一个 普通 API 操作的单个请求 我们实际生活中的 许多 API 有数百种操作 包含丰富的请求和响应类型、 头字段、参数等等 为每个操作 编写这样的代码会很重复、 冗长且容易出错 而且 代码库中的所有这些冗长内容 会影响 App 的核心逻辑
有了 OpenAPI 你可以 使用工具来生成大部分代码 这样你就可以重点关注 用户会交互的代码 我们将使用我们的示例 API 来探索一个 OpenAPI 文档 每个 OpenAPI 文档都声明了 它所使用的 OpenAPI 规范的版本 它提供了关于 API 的元数据 包括其名称、版本 和服务器 URL 列表 然后 它列出了 构成 API 的路径和 HTTP 方法 该 API 只有一个 名为 getGreeting 的操作 它定义了 greet 路径上 GET 方法的行为 在本示例中 服务器总是以 200 作为响应 这是表示 OK 的 HTTP 状态码 并返回一个 JSON 对象 它使用 JSON Schema 定义
在本示例中 我们使用简单的例子 但操作可以有多个响应 具有不同的状态码和内容类型 这样你可以记录下所有场景 包括出错时会发生什么 如果操作接受参数 这些参数也可以包含在 OpenAPI 文档中
该操作支持一个名为“name”的 可选查询参数 该字符串值用于个性化问候语
在 Swift OpenAPI Generator 的帮助下 我们可以用更少的代码 实现相同的 API 调用
我们可以使用类型安全输入 输出值是充血枚举类型 因此编译器可以帮助我们确保 处理每一种记录下的响应 和内容类型 响应主体中的相关值 是具有类型安全属性的 值类型 所有与输入编码、 请求发出、响应解析 和输出解码相关的冗长步骤 都可以由生成的代码处理
Swift OpenAPI Generator 是一个在构建时运行的 Swift 软件包插件 这意味着生成的代码始终 与 OpenAPI 文档同步 无需提交到源代码储存库中 要进一步了解更多关于 Swift 软件包插件的信息 请查看 “认识 Swift 软件包插件”讲座 让我们来看看如何在一个 简单的 iOS App 中 使用 Swift OpenAPI Generator 为此 我们需要一个可调用的 API 在本示例中 我们将调用一个简单的 API 随机返回 十个猫脸表情符号中的一个 我们将 从 SwiftUI App 模板开始 并将示例内容替换为一个大表情符号 和一个按钮 每次轻点该按钮都会 从服务器获取一个新的表情符号 我们已经有一个运行中的服务器 监听在本地主机上 我们可以使用 curl 从终端查询
不可否认 这是一个很棒的 API 但它更好的一点在于 它使用 OpenAPI 定义 让我们用一只非常不同的猫 来展示这项服务的 OpenAPI 文档
该 API 有一个名为 getEmoji 的单一操作 我们将从 App 中 调用该操作来更新 UI 让我们切换到 Xcode 来开始
这个示例 iOS App 有一个 基本的 UI 使用 SwiftUI 定义 我们可以在 Xcode 预览中看到 在接下来的几分钟中 我们将用动态内容 替换 UI 组件 我们将从服务器 获取这些动态内容 我们将使用 Swift Open API Generator 来简化 我们手动编写的代码 以便进行 API 调用 首先 我们将所需的 软件包依赖项添加到项目中 然后 我们将配置我们的目标 以便使用插件生成代码 并添加 OpenAPI 文档 和插件配置文件 到我们的目标源代码目录 项目配置完成后 我们将替换 UI 组件 并使用生成的 Client 类型 调用服务器的 API 要配置你的 App 以使用 Swift OpenAPI Generator 请导航至“项目编辑器” 选择“软件包依赖项”选项卡 然后点击以添加新的依赖项
在演示中 我们使用本地软件包集合 但你可以在讲座笔记中 找到软件包的 URL 首先 我们将 swift-openapi-generator 添加为依赖项 它提供软件包插件
然后 我们将 swift-openapi-runtime 添加为依赖项 它提供了生成代码所使用的 通用类型和抽象
由于生成的代码没有绑定到 任何特定的 HTTP 客户端库 我们需要为 我们想使用的库选择一个集成包 我们正在构建一个 iOS App 所以我们将使用 URLSession 包 但请你参阅文档来查看其他示例 以及如何编写自己的软件包 有了这些依赖项 我们就可以配置目标来使用 OpenAPI Generator 插件了 在“目标设置”中 选择“构建阶段” 并展开名为 “Run build-tool plugins”的部分 点击以添加新插件并从列表中选择 OpenAPIGenerator
该插件在目标源代码目录下 需要有两个输入文件: OpenAPI 文档 和一个插件配置文件 我现在将它们添加到项目中
插件配置使用简单的 YAML schema 编写 该模式指定插件应生成哪些代码 在本例中 我们将生成“types” 即从 OpenAPI 文档中 派生出来的可复用类型 我们还将生成客户端代码 它可用于 与任何 HTTP 客户端 进行 API 调用 我们将切换回 ContentView.swift 它将重新编译我们的项目 这样生成的代码就可以 在我们的 App 中使用了
作为一项安全措施 你在首次使用时会被要求 信任该插件
现在我们已经重新编译了项目 我们可以替换 UI 组件 并使用生成的 Client 类型 向服务器进行 API 调用 并更新视图
首先 我们将为表情符号添加一个 新的 state 属性到视图中 并使用占位符值对其进行初始化 然后 我们将用包含表情符号的 文本视图替换这个地球图像 用按钮替换 “Hello, world”消息 并为视图设置按钮样式
生成的代码提供了一个 名为 Client 的类型 你可以用它来进行 API 调用 但首先 我们需要导入 OpenAPI 运行时和传输模块
现在 我们可以为视图 添加一个客户端属性和初始化程序 将其配置为使用 OpenAPI 文档中定义的 本地主机服务器 URL
现在 我们将添加一个函数 使用该客户端对服务器 进行 API 调用
这就是我们进行 API 请求 需手动编写的全部代码 其他一切都会由生成的代码处理 响应是一个枚举类型的值 它模拟了所有文档中的响应 和内容类型 从而鼓励我们 处理所有场景 因此 我们需要使用 switch 语句从响应主体中 提取表情符号
这里缺了什么 编译器告诉我们 我们还没有处理所有的场景 我们让 Xcode 来填补 缺失的 switch 案例
如果服务器响应的内容 没有在 OpenAPI 文档中指定 你仍然有机会来妥善处理它 在这个演示中 我们将在控制台输出一个警告 并将我们的表情符号 更新为猫以外的东西
现在我们可以 在轻点按钮时调用该函数
我们可以使用按钮获取新的 猫咪表情符号并更新我们的 UI
随着新功能被添加到服务器上 其 API 也会随之发展 如果服务器 使用 OpenAPI 进行记录 那么 Swift OpenAPI Generator 可以让用户轻松使用 你 App 中的这些新功能 让我们举例说明如何随着 OpenAPI 文档的变化更新 App
当涉及到表情符号时 多多益善 因此我们扩展了服务 API 以获取新的可选查询参数 count 该参数可用于 获取多个表情符号
我们用另一个按钮来扩展我们的 App 可以获取三个猫表情符号而非一个
首先 我们将在 OpenAPI 文档中添加一个参数 一旦我们重新编译项目 该参数就可以 在 App 中使用 然后 我们将创建一个新按钮 使用该参数进行 API 调用 我们首先将新参数 添加到 OpenAPI 文档中
该参数名为“count” 这是一个可选参数 它作为 URL 查询的 一部分提供 是一个整数值 让我们回到 ContentView.swift 并扩展 updateEmoji 函数 使其也接受一个参数
让我们在调用 API 时 使用该参数
我们将复制该按钮 并将标签更改为“More cats”
当轻点该按钮时 我们将调用相同的函数 但这次的计数为 3
现在 在预览中 我们可以轻点 “get cat”来获取一个猫表情符号 或者轻点“More cats” 来获取三个猫表情符号 一直以来 我们都在 向真实服务器发出请求 但这并不总是可行或可取的 尤其是在开发过程中 因为生成的 Client 类型 遵守 Swift 协议 所以我们很容易 编写一个不需要网络连接 或传输库的 mock 生成的协议被命名为 APIProtocol 因此我们将首先 定义一个采用该协议的 新 MockClient 类型 然后 我们将更新视图 使其泛型于任何 遵守 APIProtocol 的类型 并更新初始化程序以支持依赖注入 然后我们将在 Xcode 中 预览 UI 时使用 MockClient 我们从声明我们的 MockClient 类型开始
因为我们声明该类型 采用 APIProtocol 编译器将确保它满足协议要求 我们将让 Xcode 为 API 操作 添加缺失的处理器
我们将添加业务逻辑 返回机器人表情符号 以区别于真正的服务
现在我们可以使视图 泛型于遵守该协议的类型 并更新客户端属性 以使用泛型类型参数
我们将添加一个初始化程序 它需要一个客户端作为参数 我们将用一个泛型 where 子句 更新现有的初始化程序 所以如果没有提供客户端 我们将使用和以前一样的客户端
当我们的 App 发布后 它将继续使用真实服务器 但现在我们可以在 Xcode 中 预览 UI 时注入 MockClient
现在 当我们在 UI 预览中轻点按钮时 我们将获取机器人表情符号 而不是猫表情符号 并且不需要 网络连接或一个正在运行的服务器
直到我们添加 mock 客户端 我们的 iOS App 一直在向 运行在本地计算机上的 真实服务器发出请求 该服务器也是在 Swift OpenAPI Generator 的帮助下用 Swift 编写的 服务器是一个简单的 Swift 软件包 它使用 Swift OpenAPI Generator 软件包插件生成代码 为了使用生成的 服务器代码 我们定义了一个 遵守生成协议的类型 命名为 APIProtocol 并刚好实现了 我们 API 操作的业务逻辑 为了配置服务器 我们使用了一个生成的函数 registerHandlers 它将收到的 API 操作 HTTP 请求连接到 提供业务逻辑的处理器 让我们来看看
如果我们展开控制台 就可以看到来自 我们的 iOS App 演示的实际请求
这就是为实现服务器 需要手动编写的所有 Swift 代码 我们使用 OpenAPI 不单是用来记录这项服务 而是从 OpenAPI 文档开始 使用 Swift OpenAPI Generator 来简化实现 API 规范的 服务器的编写过程
我们定义了一个类型 它遵守生成的 APIProtocol 且刚好提供了 我们 API 操作的业务逻辑 我们还使用了一个生成的函数 将其方法注册到 API 端点的 HTTP 服务器 在本演示中 我们使用的是 Vapor 这是一个适用于 Swift 的开源 Web 框架 但是 生成的代码可以用于任何为 Swift OpenAPI Generator 提供集成包的 Web 框架 你可以查看文档 来了解其他选项 以及如何编写自己的代码
在我们的主函数中 我们创建一个新的 Vapor Application 用来创建一个 OpenAPI 传输 然后 我们创建一个 处理器类型的实例 并使用生成的 registerHandlers 函数 在 HTTP 服务器中为我们的每个 API 操作设置路由 否则我们必须手动完成 最后 我们运行 Vapor App 和手动配置的方式是一样的
Swift 是一种非常适合 服务器开发的语言 如果你想进一步了解更多关于 用 Swift 编写后端服务的信息 请查看“使用 Xcode 进行服务器端开发”讲座
让我们来看看如何配置软件包 以使用 Swift OpenAPI Generator 服务器 以 Swift 软件包的形式实现 并使用 Package.swift 文件进行定义 该软件包仅有一个可执行目标 名为 CatService 它使用了 Swift OpenAPI Generator 插件 生成的服务器代码 依赖于运行时库中的 常见类型和抽象 并且可以与任何提供集成包的 Web 框架一起使用 因此该目标的依赖项有 swift-openapi-runtime、 swift-openapi-vapor 和 vapor 本身 在目标源代码目录中 我们添加了 OpenAPI 文档 与我们在演示 iOS App 中 使用的文档相同 还有插件配置文件 对于这个目标 我们生成类型和服务器存根 让我们看看规范驱动开发是如何简化 为该服务添加新功能的
猫咪表情符号很不错 但许多证据表明 互联网主要是 为交流猫咪视频而建立的 因此我们将在服务器中添加该功能
通过规范驱动开发 添加一个新的 API 操作 只需要两个步骤 首先 我们将新的操作 添加到 OpenAPI 文档中 然后 由于生成的协议 现在有一个新的函数要求 编译器会要求我们 在处理器上定义一个方法 并实现业务逻辑 在开始之前 我们首先需要一个猫咪视频 我已经将其添加到我们目标的 Resources 文件夹中
然后我们去 OpenAPI 文档 添加新的操作
该操作命名为 getClip 有一个二进制格式的响应 其内容类型表明 响应体包含视频数据
当我们尝试重新编译 我们的软件包时 它会失败
这是因为我们的处理器 不再遵守生成的协议 因为它没有 为新的操作提供一个函数 我们将让 Xcode 为我们填写一个协议存根 我们将提供业务逻辑 它从视频资源文件中读取字节 并返回一个 带有二进制主体的 OK 响应 注意生成的类型安全代码只允许 从该函数返回 二进制响应体 因为这是 OpenAPI 文档中为该操作指定的 当我们重新编译我们的 软件包时 它会成功 我们可以重新发布我们的服务器 如果我们切换到 Safari 浏览器 我们可以测试新的 API 端点
今天我们看到 使用 OpenAPI 记录服务 有助于消除歧义 并实现规范驱动开发 我们还展示了 Swift OpenAPI Generator 如何简化 iOS App 中 服务器 API 的工作 最后 我们看到了 Swift 的语言特性 和不断发展的 Swift-on-server 生态系统 是如何使其成为 实现后端服务的绝佳选择 这就是为什么 Swift OpenAPI Generator 是开源的 并且可以在 GitHub 上获取 你可以在这里进一步了解更多信息 甚至随着项目的 持续发展 为其做出贡献 感谢你观看本讲座 今天的猫咪时间就到这里!
-
-
4:17 - Example OpenAPI document
openapi: "3.0.3" info: title: "GreetingService" version: "1.0.0" servers: - url: "http://localhost:8080/api" description: "Production" paths: /greet: get: operationId: "getGreeting" parameters: - name: "name" required: false in: "query" description: "Personalizes the greeting." schema: type: "string" responses: "200": description: "Returns a greeting" content: application/json: schema: $ref: "#/components/schemas/Greeting"
-
7:05 - CatService openapi.yaml
openapi: "3.0.3" info: title: CatService version: 1.0.0 servers: - url: http://localhost:8080/api description: "Localhost cats 🙀" paths: /emoji: get: operationId: getEmoji parameters: - name: count required: false in: query description: "The number of cats to return. 😽😽😽" schema: type: integer responses: '200': description: "Returns a random emoji, of a cat, ofc! 😻" content: text/plain: schema: type: string
-
8:03 - Making API calls from your app
import SwiftUI import OpenAPIRuntime import OpenAPIURLSession #Preview { ContentView() } struct ContentView: View { @State private var emoji = "🫥" var body: some View { VStack { Text(emoji).font(.system(size: 100)) Button("Get cat!") { Task { try? await updateEmoji() } } } .padding() .buttonStyle(.borderedProminent) } let client: Client init() { self.client = Client( serverURL: try! Servers.server1(), transport: URLSessionTransport() ) } func updateEmoji() async throws { let response = try await client.getEmoji(Operations.getEmoji.Input()) switch response { case let .ok(okResponse): switch okResponse.body { case .text(let text): emoji = text } case .undocumented(statusCode: let statusCode, _): print("cat-astrophe: \(statusCode)") emoji = "🙉" } } }
-
9:48 - CatServiceClient openapi-generator-config.yaml
generate: - types - client
-
13:24 - Adapting as the API evolves
import SwiftUI import OpenAPIRuntime import OpenAPIURLSession #Preview { ContentView() } struct ContentView: View { @State private var emoji = "🫥" var body: some View { VStack { Text(emoji).font(.system(size: 100)) Button("Get cat!") { Task { try? await updateEmoji() } } Button("More cats!") { Task { try? await updateEmoji(count: 3) } } } .padding() .buttonStyle(.borderedProminent) } let client: Client init() { self.client = Client( serverURL: try! Servers.server1(), transport: URLSessionTransport() ) } func updateEmoji(count: Int = 1) async throws { let response = try await client.getEmoji(Operations.getEmoji.Input( query: Operations.getEmoji.Input.Query(count: count) )) switch response { case let .ok(okResponse): switch okResponse.body { case .text(let text): emoji = text } case .undocumented(statusCode: let statusCode, _): print("cat-astrophe: \(statusCode)") emoji = "🙉" } } }
-
15:06 - Testing your app with mocks
import SwiftUI import OpenAPIRuntime import OpenAPIURLSession #Preview { ContentView(client: MockClient()) } struct ContentView<C: APIProtocol>: View { @State private var emoji = "🫥" var body: some View { VStack { Text(emoji).font(.system(size: 100)) Button("Get cat!") { Task { try? await updateEmoji() } } Button("More cats!") { Task { try? await updateEmoji(count: 3) } } } .padding() .buttonStyle(.borderedProminent) } let client: C init(client: C) { self.client = client } init() where C == Client { self.client = Client( serverURL: try! Servers.server1(), transport: URLSessionTransport() ) } func updateEmoji(count: Int = 1) async throws { let response = try await client.getEmoji(Operations.getEmoji.Input( query: Operations.getEmoji.Input.Query(count: count) )) switch response { case let .ok(okResponse): switch okResponse.body { case .text(let text): emoji = text } case .undocumented(statusCode: let statusCode, _): print("cat-astrophe: \(statusCode)") emoji = "🙉" } } } struct MockClient: APIProtocol { func getEmoji(_ input: Operations.getEmoji.Input) async throws -> Operations.getEmoji.Output { let count = input.query.count ?? 1 let emojis = String(repeating: "🤖", count: count) return .ok(Operations.getEmoji.Output.Ok( body: .text(emojis) )) } }
-
16:58 - Implementing a backend server
import Foundation import OpenAPIRuntime import OpenAPIVapor import Vapor struct Handler: APIProtocol { func getEmoji(_ input: Operations.getEmoji.Input) async throws -> Operations.getEmoji.Output { let candidates = "🐱😹😻🙀😿😽😸😺😾😼" let chosen = String(candidates.randomElement()!) let count = input.query.count ?? 1 let emojis = String(repeating: chosen, count: count) return .ok(Operations.getEmoji.Output.Ok(body: .text(emojis))) } } @main struct CatService { public static func main() throws { let app = Vapor.Application() let transport = VaporTransport(routesBuilder: app) let handler = Handler() try handler.registerHandlers(on: transport, serverURL: Servers.server1()) try app.run() } }
-
18:43 - CatService Package.swift
// swift-tools-version: 5.8 import PackageDescription let package = Package( name: "CatService", platforms: [ .macOS(.v13), ], dependencies: [ .package( url: "https://github.com/apple/swift-openapi-generator", .upToNextMinor(from: "0.1.0") ), .package( url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.1.0") ), .package( url: "https://github.com/swift-server/swift-openapi-vapor", .upToNextMinor(from: "0.1.0") ), .package( url: "https://github.com/vapor/vapor", .upToNextMajor(from: "4.69.2") ), ], targets: [ .executableTarget( name: "CatService", dependencies: [ .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"), .product(name: "OpenAPIVapor", package: "swift-openapi-vapor"), .product(name: "Vapor", package: "vapor"), ], resources: [ .process("Resources/cat.mp4"), ], plugins: [ .plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator"), ] ), ] )
-
19:08 - CatService openapi.yaml
openapi: "3.0.3" info: title: CatService version: 1.0.0 servers: - url: http://localhost:8080/api description: "Localhost cats 🙀" paths: /emoji: get: operationId: getEmoji parameters: - name: count required: false in: query description: "The number of cats to return. 😽😽😽" schema: type: integer responses: '200': description: "Returns a random emoji, of a cat, ofc! 😻" content: text/plain: schema: type: string
-
19:10 - CatService openapi-generator-config.yaml
generate: - types - server
-
20:11 - Adding an operation to the OpenAPI document
openapi: "3.0.3" info: title: CatService version: 1.0.0 servers: - url: http://localhost:8080/api description: "Localhost cats 🙀" paths: /emoji: get: operationId: getEmoji parameters: - name: count required: false in: query description: "The number of cats to return. 😽😽😽" schema: type: integer responses: '200': description: "Returns a random emoji, of a cat, ofc! 😻" content: text/plain: schema: type: string /clip: get: operationId: getClip responses: '200': description: "Returns a cat video! 😽" content: video/mp4: schema: type: string format: binary
-
20:26 - Adding a new API operation
import Foundation import OpenAPIRuntime import OpenAPIVapor import Vapor struct Handler: APIProtocol { func getClip(_ input: Operations.getClip.Input) async throws -> Operations.getClip.Output { let clipResourceURL = Bundle.module.url(forResource: "cat", withExtension: "mp4")! let clipData = try Data(contentsOf: clipResourceURL) return .ok(Operations.getClip.Output.Ok(body: .binary(clipData))) } func getEmoji(_ input: Operations.getEmoji.Input) async throws -> Operations.getEmoji.Output { let candidates = "🐱😹😻🙀😿😽😸😺😾😼" let chosen = String(candidates.randomElement()!) let count = input.query.count ?? 1 let emojis = String(repeating: chosen, count: count) return .ok(Operations.getEmoji.Output.Ok(body: .text(emojis))) } } @main struct CatService { public static func main() throws { let app = Vapor.Application() let transport = VaporTransport(routesBuilder: app) let handler = Handler() try handler.registerHandlers(on: transport, serverURL: Servers.server1()) try app.run() } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。