⚒️

Swift OpenAPI Generator で Swift でのスキーマ駆動開発がもっと便利になる - はじめに -

2023/06/20に公開

この記事では WWDC23 で発表された Swift OpenAPI Generator を取り上げます。

OpenAPI とは

OpenAPI Specification(OAS) は、 HTTP API のインターフェース定義です。サーバーサイドとの仕様の認識合わせにもってこいで、ドキュメントやコード生成などエコシステムも充実しています。

yaml や json 形式で API の仕様を記載することができます。

Swift OpenAPI Generator とは

WWDC23 では Swift 特化の Generator、Swift OpenAPI Generator が発表されました。Swift Package plugin として提供されたことでコードの生成から利用を、Xcodeを離れることなくスムーズに行えます。(ちなみに推奨されている使い方は plugin ですが、CLI として実行することは可能です。詳しくはこちら

Swift Package plugin は、昨年の WWDC22 で発表された Swift Package または Xcode 上で動く Swift スクリプトです。
今までアプリ側では Build Phases で Run Script を実行することができましたが、Swift Package にはビルド前やビルド中に実行できる処理を定義できませんでした。

ユースケースとしては、コード生成やリリース作業の自動化などが紹介されていました。

今回の Swift OpenAPI Generator の登場で、昨年の発表はこのためだったのかーと点が線になる感覚を味わいました。

話を戻します。

Swift Package Generator は以前から存在していた OpenAPI Tools が提供している OpenAPI Generator と比較すると、spec からコードを生成するということ自体は同じですが、導入の手軽さ、利用のしやすさがメリットです。

これ以降、特に明示しない限り、Generator といえば Swift OpenAPI Generator のことを指すこととします。

実際にやってみる

では実際にやってみます。今回はクライアントコードの生成、つまり iOS アプリで使用される Swift コードを生成を行います。サーバーサイドで使用するコードの生成も可能ですが、長くなってしまうのでクライアントコードのみを扱います。

WWDC23 の「Meet Swift OpenAPI Generator」のセッションでも実際に使い方の手順が説明されています。またわかりやすいチュートリアル(英)も用意されています。そちらも詳しいのでぜひ参考にしてみてください。

導入編

1. Package Dependencies に3つの依存関係を追加する

1つ目。Swift Package Generator 本体。これが実際にコードを生成するためのロジックが入っています。

これの plugin.swiftPackage.swift は Swift Package plugin 自体の勉強にもなりますし、この plugin がどのような入出力を期待しているかも読み取れて面白いです。

https://github.com/apple/swift-openapi-generator

2つ目。Swift OpenAPI Runtime。生成されたコードから使用される、共通の型や抽象化を提供しています。

https://github.com/apple/swift-openapi-runtime

3つ目。Swift OpenAPI URLSession。生成されたコードは特定の HTTP クライアントライブラリに依存していないので、紐付けたいライブラリを選択することができます。今回 iOS アプリのクライアントコードを生成するのでこれを選びます。

https://github.com/apple/swift-openapi-urlsession

GitHub の README ではいくつか紹介されていて、サーバーサイドであれば、Vapor やHummingbird といった選択肢があります。

2. Build Phases を修正する

次に、Build Phases の Run Build Tool Plug-ins で、OpenAPIGenerator を追加します。これでコードの生成が、ファイルのコンパイルの前に行われます。

ちなみに他の Run Script や Compile Sources と違い、この Run Build Tool Plug-ins は順番を前後に移動することができません。Run Script をこれの前に実行しようと移動させようとしたのですが、できませんでした。

3. OpenAPI のドキュメント2つを追加する

次にコード生成のための重要なインプットである、2つのドキュメントをプロジェクトに追加します。

1つ目は、もちろん spec の yaml または json です。

ファイル名には制限があり、openapi.yamlまたはopenapi.jsonというファイル名でなければいけません。

もし使用すると、No OpenAPI document found in the target named 'xxx' というエラーが発生して、ビルドが失敗します。

2つ目は、openapi-generator-config.yaml というファイルです。これには生成するコードの種類を定義します。

openapi-generator-config.yaml
generate:
  - types
  - client # typesのコードに依存する
#  - server # typesのコードに依存する。サーバーのコードを生成するときに使用する

今回は iOS アプリで使用するクライアントコードを生成するので、typesclient を定義します。サーバーサイドのコードを生成するときは typesserver を定義します。

4. plugin を信頼する

初めて plugin を使用するときは、以下のような警告と信頼し、有効にするかどうかを聞かれます。「Trust & Enable All」 を選択します。

5. ビルドする

ここまで完了したら、一旦ビルドします。
ビルドログを確認すると、Generator がどのような設定でコード生成まで行おうとしているかがわかります。

コードは DerivedData の中に生成され、生成されるファイルは Types.swiftClient.swift です。openapi-generator-config.yaml で定義した通りですね。

このスクショでは、Generator を動作させるのは初めてではなく、特に openapi.yaml も変更せずにビルドしなおしたため、File Types.swift: unchangedFile: Client.swift: unchanged のログが出力されています。


導入はこれで完了です! plugin で提供されていることにより、Xcode を離れることなくスムーズに導入できました。

コード利用編

早速生成されたコードを使って実装していきます。

まずは必要なモジュール2つをインポートします。

ContentView.swift
import OpenAPIRuntime
import OpenAPIURLSession

次に、生成されたコードは Client という型を提供しているので、それを初期化します。
ここでは引数にサーバーURL と 実際にネットワークを使用して HTTP 操作を行う HTTP ライブラリを指定します。

struct ContentView: View {
    var body: some View {
    // 省略
    }

    let client: Client

    init() {
        self.client = Client(
            serverURL: try! Servers.server1(),
            transport: URLSessionTransport()
        )
    }
}

ちなみに ClientAPIProtocol に準拠しており、上記2つ以外にも受け取れる引数があります。

そして実際に API リクエストを実装していきます。ここではpostAppStateというメソッドを実装し、その処理で API を呼びます。

Client型が準拠している APIProtocol には自動生成によってpostAppStoreState という API が定義されているので、それを呼び出します。
今回呼びたい API は POST メソッドでリクエストボティが存在するので、それを渡しています。

    // 省略

    func postAppState() async throws {
        let response = try await client.postAppStoreState(Operations.postAppStoreState.Input(
            body: .json(Components.Schemas.PostAppStoreStateRequestBody(channelID: "C01JJKQPKCK", appIDs: ["1673161138", "1665806389"]))
        ))
    }

次にレスポンスの処理です。レスポンスは、 enum で実装されていますので、switch でケースごとに処理をしていきます。
spec で定義したステータスコードごとのレスポンスに加えて、.undocumentedという定義されていないステータスコードが来た時の処理をするケースも追加されています。

    // 省略

    func postAppState() async throws {
	// 省略
    
	switch response {
        case let .ok(okResponse):
            switch okResponse.body {
            case .json(let json):
                // do something
            }
        case let .unauthorized(error):
            print("unauthorized: \(error)")
            // do something
        case let .badRequest(json):
            print("bad request:", json)
	    // do something
        case .undocumented(statusCode: let statusCode, let payload):
            print("undocumented: \(statusCode)\n \(payload)")
            // do something
        }
    }

実装はこれで完了です!コード生成によってリクエストに必要なメソッドが既に用意されていること、レスポンスも型安全な状態で spec が正しければ、少なくとも実装で間違えることはありません。

運用編

openapi.yaml に変更を加えた場合を考えてみます。

ビルドプロセスの中に含まれているということからもお分かりかと思いますが、コードを生成し直すには ビルドし直すだけ でOKです。

OpenAPI Tools の OpenAPI Generator を使用している場合は、コード生成のコマンドを打ってどこかにプッシュして・・・ととにかくやることが多いです。そこを自動化したとしても、Swift OpenAPI Generator のようにスムーズにはいかなかったと思います。

また生成されたコードを Git にプッシュする必要もありません。そもそも前述したように DerivedData の中に生成されるので、git の追跡範囲外になっています。

おわりに

Swift OpenAPI Generator の導入から運用編までを解説しました。

Swift Package plugin の ビルドツールプラグインとして使用可能なことで、OpenAPI Tools の Generator と比較して導入が非常に楽になりました。使い慣れている Xcode から離れずに導入が可能なのは iOS エンジニアには嬉しいことだと思います。

次の記事では、実際に使ってみての注意点や対策、メリット・デメリットについて書こうと思います。

ここまで見てくださり、ありがとうございました!😊

Discussion