WWDC Quick Look 💓 By SwiftGGTeam
Meet Swift OpenAPI Generator

Meet Swift OpenAPI Generator

观看原视频

Highlight

Swift OpenAPI Generator 是一个 Swift Package 插件,在构建时根据 OpenAPI 文档自动生成类型安全的客户端和服务端代码,消除手写网络请求样板代码,并确保代码与 API 文档始终保持同步。

核心内容

调用一个服务端 API 需要考虑很多事:base URL 是什么?路径怎么拼?HTTP 方法用 GET 还是 POST?参数放 query 还是 body?返回的数据结构是什么?

手写文档容易过时,看源码又费时费力。OpenAPI 用机器可读的 YAML 或 JSON 描述 API,成为服务端和客户端之间的单一事实来源。

01:58

手写 API 调用的痛点

一个最简单的 API 调用,手写代码需要这么多步骤:

// 1. 解析 base URL
var components = URLComponents(string: "http://localhost:8080/api")!
// 2. 添加路径
components.path = "/greet"
// 3. 添加 query 参数
components.queryItems = [URLQueryItem(name: "name", value: "World")]
// 4. 构造请求
let request = URLRequest(url: components.url!)
// 5. 发起请求
let (data, response) = try await URLSession.shared.data(for: request)
// 6. 验证响应
guard let httpResponse = response as? HTTPURLResponse,
      httpResponse.statusCode == 200 else {
    throw APIError.invalidResponse
}
// 7. 解码 JSON
struct Greeting: Decodable {
    let message: String
}
let greeting = try JSONDecoder().decode(Greeting.self, from: data)
// 8. 使用结果
print(greeting.message)

这只是一个最简单的 GET 请求。真实世界的 API 有上百个操作,各种请求头、认证、错误码、内容协商。手写这些代码重复、冗长、容易出错。

02:55

OpenAPI 文档

OpenAPI 文档描述 API 的完整契约:

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"

文档包含:OpenAPI 版本、API 元数据、服务器地址、路径和操作、参数定义、响应定义。一个文档就是 API 的完整规范。

04:17

生成的客户端代码

Swift OpenAPI Generator 在构建时读取 OpenAPI 文档,自动生成类型安全的 Swift 代码。使用生成的客户端:

import SwiftUI
import OpenAPIRuntime
import OpenAPIURLSession

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 = "🙉"
        }
    }
}

关键点:

  • Client 是生成的类型,包含所有 API 操作的方法
  • Servers.server1() 从 OpenAPI 文档的服务器定义生成
  • Operations.getEmoji.Input() 是类型安全的请求输入
  • 响应是枚举类型,.ok.undocumented 覆盖文档中定义和未定义的响应
  • 编译器强制你处理每种响应情况

08:03

详细内容

项目配置

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/apple/swift-openapi-urlsession",
            .upToNextMinor(from: "0.1.0")
        ),
    ],
    targets: [
        .executableTarget(
            name: "CatService",
            dependencies: [
                .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"),
                .product(name: "OpenAPIURLSession", package: "swift-openapi-urlsession"),
            ],
            plugins: [
                .plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator"),
            ]
        ),
    ]
)

关键点:

  • swift-openapi-generator 是构建插件,在编译时生成代码
  • swift-openapi-runtime 提供运行时类型和协议
  • swift-openapi-urlsession 是 URLSession 传输层实现
  • 插件在 plugins 中声明,构建时自动运行

18:43

生成配置

创建 openapi-generator-config.yaml 控制生成内容:

generate:
  - types
  - client

types 生成数据类型(请求输入、响应输出、模型结构体)。client 生成客户端代码。如果要实现服务端,换成 server

09:48

带参数的 API 调用

OpenAPI 文档中定义的参数会自动生成类型安全的输入类型:

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 = "🙉"
    }
}

Operations.getEmoji.Input.Query(count: count) 是类型安全的 query 参数构造器。如果 OpenAPI 文档把 count 定义为 integer,这里传 String 会编译报错。

13:24

测试用的 Mock Client

生成的 APIProtocol 协议让 Mock 变得简单:

struct ContentView<C: APIProtocol>: View {
    @State private var emoji = "🫥"
    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)
            )
        )
        // ...
    }
}

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

ContentView 用泛型参数 C: APIProtocol,生产环境用 Client,测试环境用 MockClient。SwiftUI 预览也可以直接注入 MockClient

15:06

服务端实现

同样的 OpenAPI 文档也可以生成服务端代码。实现 APIProtocol 即可:

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()
    }
}

关键点:

  • Handler 遵循 APIProtocol,实现所有操作
  • input.query.count 是类型安全的参数访问
  • 返回 .ok(...) 构造类型安全的响应
  • registerHandlers(on:serverURL:) 自动注册所有路由

服务端配置把 generate 改成 server

generate:
  - types
  - server

16:58

API 演进

当 API 增加新操作时,只需更新 OpenAPI 文档:

paths:
  /emoji:
    get:
      operationId: getEmoji
      # ...
  /clip:
    get:
      operationId: getClip
      responses:
        '200':
          description: "Returns a cat video!"
          content:
            video/mp4:
              schema:
                type: string
                format: binary

重新构建后,生成的代码自动包含新操作。服务端实现需要添加对应的方法:

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

编译器会强制你实现所有文档中定义的操作,不会遗漏。

20:11

核心启发

把现有手写网络层迁移到 OpenAPI 生成

如果你的项目有 REST API 调用层,把接口文档转成 OpenAPI YAML,用 Swift OpenAPI Generator 替换手写代码。一次迁移,长期受益——API 变更时只需改文档,重新构建即可。

入口:从现有的 API 文档或 Postman 集合导出 OpenAPI 文档,创建 openapi-generator-config.yaml 配置生成客户端代码。

为内部微服务建立统一的 API 契约

团队内部多个服务互相调用时,用 OpenAPI 文档作为契约。服务端和客户端从同一份文档生成代码,确保双方对 API 的理解一致。

入口:在 Git 仓库中维护 api.yaml,CI 构建时验证文档合法性并生成代码。

用 MockClient 做 UI 测试和预览

SwiftUI 预览和单元测试经常需要模拟网络响应。用 MockClient 实现 APIProtocol,返回固定数据,让预览和测试不依赖真实网络。

入口:为每个主要 View 创建对应的 MockClient 变体,在 #Preview 和测试用例中注入。

用 Vapor + OpenAPI 快速搭建后端

有 iOS 开发经验的团队可以用 Swift 写后端。Vapor + Swift OpenAPI Generator 让你专注于业务逻辑,路由注册、参数解析、响应构造都自动生成。

入口:创建 Vapor 项目,添加 swift-openapi-vapor 依赖,实现 APIProtocol 即可。

关联 Session

评论

GitHub Issues · utterances