Swift OpenAPI Generator を使って、モックサーバーアプリをローカルで起動する
こんにちは、takeshi-p0601 - Qiita です。普段iOSアプリケーションの開発をメインに業務を行っています。
Vision Pro の話が印象深い今年のWWDCでしたが、その他にもSwiftの言語仕様やXcodeの改善、UIフレームワークの機能向上など気になるものがたくさんありました。本記事ではその中の一つである Swift OpenAPI Generator と、それを利用して作成した簡易モックサーバーアプリをローカルで起動することについて書きます。
※ この記事で記載しているソースコードは、Swift OpenAPI GeneratorのWWDCセッション中に使用されたソースコードや、公式のサンプルコードなどを参考にしています
- Swift OpenAPI Generator とは
- ツールの簡単な紹介
- サーバーサイドのコードも自動生成ができる
- 公式のサンプルコードを使って、ローカルにサーバーアプリを起動する
- サーバーサイドアプリの実装方法
- 良かった点
- 気になる点
- 今後試したいこと/調べてみたいこと
- 最後に
Swift OpenAPI Generator とは
OpenAPIの仕様に準拠したドキュメントファイルの内容から、Swiftソースコードを自動で生成してくれるツールで、ビルド時に実行されるSwift Package Pluginです。 こちらでソースコードを確認することができます。※Swift Package PluginとはWWDC2022でも発表のあった比較的新しいツールで、この記事では深掘りしませんが、概要を掴むために下記のリンクが役立ちました。
既存でもOpenAPIドキュメントファイルからソースコードを自動生成するツールはありますが、AppleがSwiftを使用して公式的に開発しているツールであることや、Swift Package Pluginとして提供されていることから、Xcodeとの親和性も高く、将来的に期待できそうです。そのためiOSアプリやMacOSアプリなどを開発する場合に、Web API関連のソースコードを自動生成するツールとして、スタンダードなものになっていくことが予想されます。
ツールの簡単な紹介
詳細な手順は省きますが、ツールがどのようなものか軽く紹介します。
何らかのiOSアプリがAPIリクエストを送信してレスポンスを受け取りたいような、クライアントサイドの処理を実装するとして、 まず下記のようなOpenAPIドキュメントファイルを用意します。
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
その後、Xcodeで上記のドキュメントファイルの組み込みやツールの設定ファイルの追加、そのほか必要な手順を済ませると、ビルド時にソースコードを自動生成するツールが実行され、
ソースコードが自動で生成されます。
それにより、iOSアプリのプロダクトコードに自動生成されたAPIリクエスト処理とそのレスポンスを処理できるプログラムを組み込めるようになります。
API通信処理を組み込んだ後でデバックビルドし、そのログを追ってみると
SwiftCompile normal arm64 /Users/takeshikomori/me/iOS/testSwiftOpenAPIGenerator/testSwiftOpenAPIGenerator/testSwiftOpenAPIGeneratorApp.swift (in target 'testSwiftOpenAPIGenerator' from project 'testSwiftOpenAPIGenerator') cd /Users/takeshikomori/me/iOS/testSwiftOpenAPIGenerator /Applications/Xcode15beta2.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift-frontend -c .... /Users/takeshikomori/Library/Developer/Xcode/DerivedData/testSwiftOpenAPIGenerator-himwceovqvysargtcarobfkdexzk/SourcePackages/plugins/testSwiftOpenAPIGenerator.output/testSwiftOpenAPIGenerator/OpenAPIGenerator/GeneratedSources/Client.swift
DerivedData/.../SourcePackages/plugins/testSwiftOpenAPIGenerator.output/testSwiftOpenAPIGenerator/OpenAPIGenerator/GeneratedSources/Client.swift
とあるように、 DerivedData/
に自動生成されたソースコードが生成され、それを元にコンパイルするので、デフォルトでプロジェクトフォルダ直下に自動生成されることはなく、メインアプリターゲットのソースファイルとして管理することはないようです。
サーバーサイドのコードも自動生成ができる
Swift OpenAPI Generatorのセッション は、前のセクションで説明したようなクライアントサイドアプリで使えるSwiftソースコードを自動生成する話がメインですが、セッションの後半ラスト5分ほどでサーバーサイドアプリのソースコードにおいても、自動生成できる紹介がありました。
恥ずかしながら元々セッションの目次を見ていなかったため、それに辿り着くまでpythonを使った簡易サーバープログラムを起動し、クライアントサイドアプリの自動生成ソースコードの挙動確認を行なっていました。後半のセッションをもとにサーバーサイドの自動生成プログラムの内容も試してみると、Swiftを使って簡単に少ないコードでローカルモックサーバーアプリを起動することができました。
普段クライアントサイドアプリを中心にSwiftをプログラミングするエンジニアとしては、Swiftで簡単にサーバーアプリを起動できたことは良い体験でしたので、その辺りを紹介します。(サーバーサイドの実装には、 Vapor というSwiftで主に実装されたオープンソースのWebフレームワークを前提としています。)
公式のサンプルコードを使って、ローカルにサーバーアプリを起動する
実装方法を紹介する前に、公式にあるサンプルコードを使用してローカルにモックサーバーアプリを起動できるので、下記の手順を踏んで軽く確認をしてみます。 ※具体的な繋ぎ込みの手順はこの次のセクションに記載しています。
試した環境
- Xcode14.3 (Swift5.8)
- Mac OS 13.4
- version: commit 7b97c0e062411ec594fad8d670a25bac1449bc06
手順
(1)ソースコードをcloneします
$ git clone https://github.com/apple/swift-openapi-generator.git
(2)clone後、下記のディレクトリに移動しておきます
$ cd swift-openapi-generator/Examples/GreetingService
(3) swift-openapi-generator/Examples/GreetingService/openapi.yaml
ファイルを開き、ベースURLを下記のようにlocalhost
に書き換えます。
title: GreetingService version: 1.0.0 servers: - - url: https://example.com/api + - url: http://localhost:8080/api description: Example paths: /greet:
(4)Xcodeの指定を、Xcode14.3
に切り替えます
$ sudo xcode-select -s /Applications/{Xcode14.3}.app/Contents/Developer
(5)ソースコードをビルドし、アプリを起動させます。
$ swift run GreetingService Building for debugging... ~~~ Build complete! (178.39s) 2023-06-28T05:03:32+0900 notice codes.vapor.application : [Vapor] Server starting on http://127.0.0.1:8080
※Exampleの例としてクライアントサイドアプリのターゲットもPackage.swiftに定義されているため、 run
サブコマンドの引数にサーバーサイドアプリのターゲット( GreetingService
)を指定してください。Exampleとしてクライアント/サーバー両方のアプリの実行ターゲットが準備されているようです。
(6)curlコマンドで疎通確認します。
下記のようにレスポンスが返されると思います。
$ curl localhost:8080/api/greet { "message" : "Hello, Stranger!" }
サーバーサイドアプリの実装方法
先の公式サンプルで示したモックサーバーアプリをlocalで立てるところまでの実装方法を紹介します。https://github.com/takeshi-p0601/TestMockServerApp にもソースコードを載せていますので、そちらもご参照ください。
試した環境
- Xcode14.3 (Swift5.8)
- Mac OS 13.4
手順
(1)適当にディレクトリを作成して、そのディレクトリに移動します。
$ mkdir TestMockServerApp && cd TestMockServerApp
(2) swift package init
を実行してserverアプリを実装するための
Swift Package を作成します。 その際、オプションの type(Package type)
に executable
を指定してください。(デフォルトでは type=library
となっています)
$ swift package init --type executable Creating executable package: TestMockServerApp Creating Package.swift Creating .gitignore Creating Sources/ Creating Sources/main.swift
(3)Package.swift
を下記のように編集します。
// swift-tools-version: 5.8 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "TestMockServerApp", 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", from: "4.76.0"), ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. // Targets can depend on other targets in this package and products from dependencies. .executableTarget( name: "TestMockServerApp", dependencies: [ .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"), .product(name: "OpenAPIVapor", package: "swift-openapi-vapor"), .product(name: "Vapor", package: "vapor"), ], path: "Sources", plugins: [ .plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator") ] ), ] )
(4) 下記の内容で、OpenAPIドキュメントファイル( openapi.yaml
)をSources
ディレクトリに追加します。
openapi: '3.0.3' info: title: TestMockServerApp version: 1.0.0 servers: - url: http://localhost:8080/api description: Example paths: /greet: get: operationId: getGreeting parameters: - name: name required: false in: query description: A name used in the returned greeting. schema: type: string responses: '200': description: A success response with a greeting. content: application/json: schema: $ref: '#/components/schemas/Greeting' components: schemas: Greeting: type: object properties: message: type: string required: - message
(5) 下記の内容で openapi-generator-config.yaml
を作成して、Sources
ディレクトリに追加します。
generate: - types - server
(6) API関連のソースコードを自動生成するためにビルドします。(Xcodeでビルドすることもできます。)
出力内容が一部異なるかもしれませんが、初回ビルド時に下記のようにOpenAPIGeneratorが実行されていることがわかります。
$ swift build ...Swift Package の Fetch など... Swift OpenAPI Generator is running with the following configuration: - OpenAPI document path: /Users/takeshikomori/TestMockServerApp/Sources/openapi.yaml - Configuration path: /Users/takeshikomori/TestMockServerApp/Sources/openapi-generator-config.yaml - Generator modes: types, server - Output file names: Types.swift, Server.swift - Output directory: /Users/takeshikomori/TestMockServerApp/.build/plugins/outputs/testmockserverapp/TestMockServerApp/OpenAPIGenerator/GeneratedSources - Diagnostics output path: <none - logs to stderr> - Current directory: /Users/takeshikomori/TestMockServerApp - Is plugin invocation: true - Additional imports: <none> File Types.swift: changed File Server.swift: changed [1876/1876] Linking TestMockServerApp Build complete! (208.55s) // 再度ビルド $ swift build Building for debugging... [4/4] Compiling plugin OpenAPIGenerator Build complete! (0.56s)
(7) Sources/main.swift
を下記のように変更します。
import OpenAPIRuntime import OpenAPIVapor import Vapor struct Handler: APIProtocol { func getGreeting( _ input: Operations.getGreeting.Input ) async throws -> Operations.getGreeting.Output { let name = input.query.name ?? "Stranger" return .ok(.init(body: .json(.init(message: "Hello, \(name)!")))) } } let app = Vapor.Application() let transport = VaporTransport(routesBuilder: app) let handler = Handler() try handler.registerHandlers(on: transport, serverURL: Servers.server1()) try app.run()
(8)ビルドして、サーバーアプリを起動させます。(Xcodeでビルド+起動させることもできます。)
$ swift run Building for debugging... Build complete! (0.53s) 2023-06-30T08:14:50+0900 notice codes.vapor.application : [Vapor] Server starting on http://127.0.0.1:8080
(9)疎通できるか確認します。
下記のようなレスポンスが返されれば問題ないと思います。
$ curl localhost:8080/api/greet { "message" : "Hello, Stranger!" } $ curl -G -d name=world localhost:8080/api/greet { "message" : "Hello, world!" }
APIの仕様の変更に対応する
APIの仕様が変わる場合も当然あると思います。その際の変更方法を紹介します。
先の手順で定義したOpenAPIの仕様から、imageバイナリデータがが一つ返される [GET] /image
エンドポイントが追加される変更があると想定して、確認します。
(10) openapi.yaml
ファイルの内容を変更します。
/image エンドポイントを追加してみます。
/image: get: operationId: getImage responses: '200': description: "Return image" content: image/png: schema: type: string format: binary
(11) ビルドします。
ログを見ると、OpenAPIドキュメントファイルが変更されたため、Types.swift
と Server.swift
に変更が入ってそうなことがわかります。
$ swift build Building for debugging... Swift OpenAPI Generator is running with the following configuration: - OpenAPI document path: /Users/takeshikomori/TestMockServerApp/Sources/openapi.yaml - Configuration path: /Users/takeshikomori/TestMockServerApp/Sources/openapi-generator-config.yaml - Generator modes: types, server - Output file names: Types.swift, Server.swift - Output directory: /Users/takeshikomori/TestMockServerApp/.build/plugins/outputs/testmockserverapp/TestMockServerApp/OpenAPIGenerator/GeneratedSources - Diagnostics output path: <none - logs to stderr> - Current directory: /Users/takeshikomori/TestMockServerApp - Is plugin invocation: true - Additional imports: <none> File Types.swift: changed File Server.swift: changed error: emit-module command failed with exit code 1 (use -v to see invocation) /Users/takeshikomori/TestMockServerApp/Sources/main.swift:5:8: error: type 'Handler' does not conform to protocol 'APIProtocol' struct Handler: APIProtocol { ^ /Users/takeshikomori/TestMockServerApp/.build/plugins/outputs/testmockserverapp/TestMockServerApp/OpenAPIGenerator/GeneratedSources/Types.swift:16:10: note: protocol requires function 'getImage' with type '(Operations.getImage.Input) async throws -> Operations.getImage.Output' func getImage(_ input: Operations.getImage.Input) async throws -> Operations.getImage.Output ^ /Users/takeshikomori/TestMockServerApp/Sources/main.swift:5:8: error: type 'Handler' does not conform to protocol 'APIProtocol' struct Handler: APIProtocol { ^ /Users/takeshikomori/TestMockServerApp/.build/plugins/outputs/testmockserverapp/TestMockServerApp/OpenAPIGenerator/GeneratedSources/Types.swift:16:10: note: protocol requires function 'getImage' with type '(Operations.getImage.Input) async throws -> Operations.getImage.Output' func getImage(_ input: Operations.getImage.Input) async throws -> Operations.getImage.Output ^ [9/10] Compiling TestMockServerApp main.swift
下の方でエラーが出てしまっているのは、プロダクトコードの実装が新たに変更されたAPIProtocolに準拠してないためです。
上記はCLIでの確認ですが、Xcodeで確認すると添付のようにエラーメッセージが表示されるので、そのFixボタンを押してメソッドを自動で追加します。
(12) 先ほど追加したメソッドの中身の実装と、imageバイナリをレスポンスとして返すために少し修正を適用させます。
こちらの変更履歴参考にしてください。
(13) 再度ビルドしてアプリを起動し、疎通確認をします。
$ swift run Building for debugging... [5/5] Linking TestMockServerApp Build complete! (2.75s) 2023-06-30T08:24:54+0900 notice codes.vapor.application : [Vapor] Server starting on http://127.0.0.1:8080
ブラウザで http://127.0.0.1:8080/api/image
にアクセスすると添付のような画像が表示されます。
==
今示したように、APIの仕様変更に対応して挙動を確認するには下記の手順になります。
- OpenAPIドキュメントを更新する
- ビルドして、変更後のOpenAPIドキュメントファイルに則ったソースコードを生成する
- 新しく生成されたソースコードを使って、プロダクトコードに繋ぎ込む
- 再度ビルドして、アプリを起動する
良かった点
- 普段使い慣れたエディタ + 言語でプログラミングできるため、特にChatGPTに頼ることなく実装できたこと。(pythonを使った簡易サーバープログラムは、簡単な文字列の操作から標準ライブラリのimportなどもすぐエラーになってしまうため、ややストレスに感じました。)
- 書くべきソースコードはそこまで多くなかったこと。
- APIProtocolインターフェースに定義された実装が強制されるので、実装もれが少なくなりそうなこと。
気になる点
一方でこのツールを使う際の気になる点として、個人的には下記がありました。
- OpenAPIドキュメントファイルは準備する必要があるので、ない場合は準備が手間なこと。
- 自動生成されたソースコードは、デフォルトで管理するような設計になってないためソースコードの変更を追従することがやや面倒です。APIの規格自体はそこまで頻繁な変更があるわけでもないように思いますが、最初のうちはバージョンごとでどういうソースコードが生成されるかや、どういった差分はあるかは確認したり、gitで管理するのもいいかもと思いました。
今後試したいこと/調べてみたいこと
今回試せなかったことで、今後下記のようなことを試したり調べてみたいです。
- Swift OpenAPI Generator を使ってAPIの結合確認の際に、簡単なレスポンスを返すモックサーバーアプリの作成
- サーバーアプリがデプロイされる前の、簡易的な動作確認用のレスポンス
- 異常なケース(大量のデータなど)のレスポンス
- Vaporの理解を深める
- 今回のGeneratorツールを利用した手順でないと、Vaporを利用することができないため。
最後に
Swift Package のエコシステムを利用しながら、クライアントサイドにとどまらない改善を進めていることを知ることができて、今後Swiftで開発するモチベーションにつながりました。WWDCのセッションはまだ視聴できてないものもあるので、引き続き確認していけたらと思います。
株式会社フォトシンスでは、一緒にプロダクトを成長させる様々なレイヤのエンジニアを募集しています。 hrmos.co
Akerun Proにご興味のある方はこちらから akerun.com