WWDC Quick Look 💓 By SwiftGGTeam
Meet Swift OpenAPI Generator

Meet Swift OpenAPI Generator

元の動画を見る

ハイライト

Swift OpenAPI Generator は、ビルド時に OpenAPI ドキュメントに基づいてタイプセーフなクライアントおよびサーバー コードを自動的に生成する Swift パッケージ プラグインで、手書きのネットワーク リクエストのボイラープレート コードを排除し、コードが常に API ドキュメントと同期していることを保証します。

主な内容

サーバーサイド API を呼び出す際には、考慮すべきことがたくさんあります。ベース URL は何ですか?パスの綴りはどうすればいいですか? HTTP メソッドには GET または POST を使用する必要がありますか?パラメータはクエリまたは本文に入れる必要がありますか?返されるデータ構造は何ですか?

手書きの文書は古くなりやすく、ソースコードを読むのは時間と労力がかかります。 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-urlsessionURLSession トランスポート層の実装です
  • プラグイン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)タイプセーフなクエリ パラメーター コンストラクターです。 OpenAPI ドキュメントに次のように書かれている場合countとして定義されるinteger、ここを通りますStringコンパイルしてエラーを報告します。

13:24

テスト用のモッククライアント

生成されたAPIProtocolプロトコルを使用するとモックが簡単になります。

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 に置き換えます。 1 回の移行で長期的なメリットがもたらされます。API が変更された場合、ドキュメントを変更して再構築するだけで済みます。

エントリ: 既存の API ドキュメントまたは Postman コレクションから OpenAPI ドキュメントをエクスポートし、openapi-generator-config.yaml生成されたクライアント コードを構成します。

社内マイクロサービス用の統一 API 契約を確立します

チーム内の複数のサービスが相互に呼び出す場合、OpenAPI ドキュメントがコントラクトとして使用されます。サーバーとクライアントは同じドキュメントからコードを生成し、両者が API について一貫した理解を確保します。

エントリ: Git リポジトリで管理されますapi.yaml, CI はドキュメントの合法性を検証し、ビルド時にコードを生成します。

UI テストとプレビューには MockClient を使用します

SwiftUI のプレビューと単体テストでは、多くの場合、ネットワーク応答をシミュレートする必要があります。使用MockClient成し遂げるAPIProtocol、プレビューとテストが実際のネットワークに依存しないように固定データを返します。

エントリ: 対応するエントリを作成しますMockClientバリアント、で#Previewそしてテストケースに注入されます。

Vapor + OpenAPI を使用してバックエンドを迅速に構築

iOS 開発の経験があるチームは、Swift でバックエンドを作成できます。 Vapor + Swift OpenAPI Generator を使用すると、ビジネス ロジックに集中でき、ルートの登録、パラメーターの解析、応答の構築がすべて自動的に生成されます。

エントリ: Vapor プロジェクトの作成、追加swift-openapi-vapor依存関係、実装APIProtocolそれでおしまい。

関連セッション

コメント

GitHub Issues · utterances