Overview: Swift OpenAPI Generator
謝辞
この記事は2024年01月25日に開催されたイベント「pixiv App Night〜エディタアプリとOpenAPIと〜」での発表内容を基にした記事です。
はじめに
Swift OpenAPI Generator
は、Appleが開発したSwift Package Plugin
であり、iOSアプリ開発の速度をさらに向上させる可能性を持っています。このツールを利用することで、OpenAPI
ドキュメントからSwift言語におけるクライアントおよびサーバーコードを自動生成させ、開発プロセスの迅速化と簡素化を実現することができます。
現代のiOSアプリ開発では、API
との連携が必要になることが多く、それを正確かつ効率的に実装していくための手段としてコードの自動生成が有効です。Swift OpenAPI Generator
は、Type Safe
なプロパティ、入出力のエンコード・デコード処理、リクエストの自動生成機能を含めた様々な機能を提供することにより、開発者が直面する多くの課題を解消します。このツールの導入により、開発者はいわゆる「儀式的なコード」の実装から解放され、別の作業により集中できるようになります。
このツールが提供する自動化機能は、最新のOpenAPI仕様(3.0.x
および3.1.x
)に対応しています。
この記事では、Swift OpenAPI Generator
の基本的な概要から、具体的なサーバー側の実装方法およびクライアント側の実装方法、さらに、Swift OpenAPI Generator
の隠蔽やヘッダーの注入方法までのプラクティスを解説していきます。これにより、iOSアプリ開発の現場でSwift OpenAPI Generator
を最大限に活用して頂ければ幸いです。
サーバ側の実装
このセクションでは、クライアント側アプリケーションをサポートするために、Vapor
フレームワークを使用してローカルで動作するサーバの実装について詳しく説明します。サーバの設計は、以下に示すOpenAPI.yaml
ファイルを基にして行われます。
OpenAPIの構成
openapi: '3.1.0'
info:
title: API For PixivAppNight
version: 1.0.0
servers:
- url: https://example.com/api
description: Example service deployment.
paths:
/v1/welcome_message:
get:
operationId: welcomeMessage
parameters:
- name: name
required: true
in: query
description: メッセージを返す
schema:
type: string
responses:
'200':
description: ok
content:
application/json:
schema:
$ref: '#/components/schemas/PixivAppNightMessage'
components:
schemas:
PixivAppNightMessage:
type: object
properties:
message:
type: string
required:
- message
このOpenAPI.yaml
ファイルには、3.1.0
バージョンのOpenAPI
仕様に従った、API For PixivAppNight
というタイトルのAPI
が定義されています。
主要APIの解説
重要な部分は、/v1/welcome_message
というパスに定義されたAPI
です。このAPI
はGET
メソッドを使用し、name
という必須のクエリパラメータを受け取ります。このパラメータは、メッセージを返すために使用され、文字列型のデータを受け取ります。レスポンスとしては、200 OK
のステータスコードと共にPixivAppNightMessage
スキーマを用いてJSON
形式のメッセージを返します。
コード自動生成の設定
サーバ側のコードと型を自動生成するためにopenapi-generator-config.yaml
が必要です。
generate:
- types
- server
types
とserver
を指定しているので、これにより型とサーバ側のコードが自動生成されます。
PackageDescriptionの内容
以下はサーバ用のPackageDescription
の内容です。
import PackageDescription
let package = Package(
name: "PixivAppNightServer",
platforms: [
.macOS(.v10_15)
],
dependencies: [
.package(url: "https://github.com/apple/swift-openapi-generator", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.0.0"),
.package(url: "https://github.com/swift-server/swift-openapi-vapor", from: "1.0.0"),
.package(url: "https://github.com/vapor/vapor", from: "4.89.0"),
],
targets: [
.executableTarget(
name: "PixivAppNightServer",
dependencies: [
.product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"),
.product(name: "OpenAPIVapor", package: "swift-openapi-vapor"),
.product(name: "Vapor", package: "vapor"),
],
plugins: [
.plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator"),
]
)
]
)
この設定をビルドするとサーバの構築に必要なプロトコルであるAPIProtocol
が自動生成ます。
APIProtocol
の概要
internal protocol APIProtocol: Sendable {
func welcomeMessage(
_ input: Operations.welcomeMessage.Input
) async throws -> Operations.welcomeMessage.Output
}
extension APIProtocol {
internal func welcomeMessage(
query: Operations.welcomeMessage.Input.Query,
headers: Operations.welcomeMessage.Input.Headers = .init()
) async throws -> Operations.welcomeMessage.Output {
try await welcomeMessage(Operations.welcomeMessage.Input(
query: query,
headers: headers
))
}
}
このプロトコルは、サーバが提供するAPI
の仕様を定義します。具体的には、welcomeMessage
メソッドを通じて、クライアントからのリクエストを処理し、適切な値を返します。
PixivAppNightAPI
の実装
APIProtocol
に準拠したPixivAppNightAPI
の実装は以下の通りです。
struct PixivAppNightAPI: APIProtocol {
func welcomeMessage(
_ input: Operations.welcomeMessage.Input
) async throws -> Operations.welcomeMessage.Output {
let name = input.query.name
let welcomeMessage = Components.Schemas.PixivAppNightMessage(message: "\(name)さん、pixivAppNightへようこそ!")
return .ok(.init(body: .json(welcomeMessage)))
}
}
このクラスは、クライアントからのname
パラメータを受け取り、PixivAppNightMessage
を生成してレスポンスとして返します。
サーバの実行
最後に、以下のようなmain
関数を実装し、サーバを起動します。
@main struct PixivAppNightServer {
static func main() async throws {
let app = Vapor.Application()
let transport = VaporTransport(routesBuilder: app)
let handler = PixivAppNightAPI()
try handler.registerHandlers(
on: transport,
serverURL: Servers.server1()
)
try await app.execute()
}
}
この実装により、ローカルでサーバが立ち上がり、API
リクエストの受付と処理が開始されます。
クライアント側の実装
このセクションでは、クライアント側の実装について詳細に説明します。ここでは、ローカル開発環境に特化した設定と実装の手順を紹介します。
OpenAPIの設定
ローカル開発を効率化するために、OpenAPI.yaml
にはローカルサーバ用のURLを追加します。
servers:
- url: https://example.com/api
description: Example service deployment.
- url: http://127.0.0.1:8080/api
description: Localhost deployment.
この設定により、ローカルホスト上で動作するサーバにクライアントがアクセスできるようになります。
コード自動生成の設定
openapi-generator-config.yaml
は以下のように設定します。
generate:
- types
- server
この設定を用いると、必要な型とクライアント側のコードが自動生成されます。
PackageDescriptionの内容
クライアント側のPackageDescription
は次の通りです。
import PackageDescription
let package = Package(
name: "PixivAppNightClient",
platforms: [.macOS(.v12), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .visionOS(.v1)],
dependencies: [
.package(url: "https://github.com/apple/swift-openapi-generator", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-openapi-urlsession", from: "1.0.0"),
],
targets: [
.executableTarget(
name: "PixivAppNightClient",
dependencies: [
.product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"),
.product(name: "OpenAPIURLSession", package: "swift-openapi-urlsession"),
],
plugins: [
.plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator"),
]
)
]
)
この設定によりビルドを行うと、APIProtocol
に準拠したClient
という名前の構造体が自動生成されます。
internal struct Client: APIProtocol {
private let client: UniversalClient
internal init(
serverURL: Foundation.URL,
configuration: Configuration = .init(),
transport: any ClientTransport,
middlewares: [any ClientMiddleware] = []
) {
// ...
)
}
// ...
internal func welcomeMessage(
_ input: Operations.welcomeMessage.Input
) async throws -> Operations.welcomeMessage.Output {
try await client.send(
// ...
)
}
}
Client
構造体の詳細解説
Client
構造体は、サーバとの通信を担う重要なコンポーネントです。この構造体はAPIProtocol
に準拠しており、以下のような機能を持っています
初期化処理
Client
のinit
メソッドは、サーバのURL
とクライアントの設定を受け取ります。serverURL
パラメータにはAPIリクエストを送信するサーバのアドレスを指定し、configuration
ではクライアントの動作をカスタマイズします。transport
引数にはリクエストの送受信を担うトランスポート層を指定し、必要に応じてミドルウェアをmiddlewares
配列に追加できます。
API
リクエストメソッド
welcomeMessage
メソッドなどのAPI
リクエスト処理は、サーバとの通信を管理します。これらのメソッドは、入力パラメータ(input
)を受け取り、サーバにリクエストを送信してレスポンスを待ちます。処理は非同期で行われ、結果はasync throws
を用いて返されます。
APIの利用方法の詳細
クライアントは、以下のコードを用いることでAPI
を叩くことができます。
let client = Client(
serverURL: URL(string: "http://127.0.0.1:8080/api")!,
transport: URLSessionTransport()
)
let response = try await client.welcomeMessage(
.init(query: .init(name: "ojun"))
)
switch response {
case let .ok(okResponse):
switch okResponse.body {
case let .json(PixivAppNightMessage):
print(PixivAppNightMessage.message)
}
case .undocumented(statusCode: let statusCode, _):
print(statusCode)
}
Client
構造体を使用してAPI
を叩くプロセスは以下のステップで構成されます:
- クライアントの初期化
-
Client
構造体のインスタンスを生成します。 - ここで
serverURL
にローカルサーバのアドレスを指定し、transport
にはURLSessionTransport
を設定します。 - これにより、クライアントは特定のサーバに接続してリクエストを送信する準備が整います。
-
-
API
リクエストの送信-
welcomeMessage
メソッドを使用してサーバにリクエストを送信します。 - このメソッドは、クエリパラメータとしてユーザー名(この例では
ojun
)を受け取り、サーバからのレスポンスを非同期的に待ちます。
-
- レスポンスの処理
-
switch
文を用いて、レスポンスを解析します。 - 成功レスポンス(
.ok
)の場合、得られたJSON
データからPixivAppNightMessage
のメッセージを抽出し、コンソールに表示します。 -
OpenAPI
で定義していないレスポンス(.undocumented
)の場合は、ステータスコードを表示します。
-
Swift OpenAPI Generator
のプラクティス
アプリ開発におけるこのセクションでは、Swiftアプリ開発においてSwift OpenAPI Generator
を利用する際のプラクティスを紹介します。
Swift OpenAPI Generator
の隠蔽
現在の実装では、開発者がSwift OpenAPI Generator
に依存したメソッドを直接呼び出しています。これは、利用者がGenerator
の実装の詳細を知る必要があることを意味しています。
この問題を解決するために、Swift OpenAPI Generator
の機能をラップする新しい構造体を導入することが推奨されます。このアプローチにより、Swift OpenAPI Generator
への直接的な依存関係が隠蔽され、API
の使用がよりシンプルになります。
PixivAppNightAPIClient
の導入
PixivAppNightAPIClient
という構造体を作成し、APIProtocol
をプライベートに保持します。
struct PixivAppNightAPIClient: Sendable {
private let client: any APIProtocol
init() {
self.client = Client(
serverURL: URL(string: "http://127.0.0.1:8080/api")!,
transport: URLSessionTransport()
)
}
}
この構造体は、init
メソッド内でClient
を初期化し、内部でAPI
呼び出しを処理します。これにより、API
の利用がより簡単かつテストしやすくなります。
初期化メソッドの多様性
PixivAppNightAPIClient
に複数の初期化メソッドを提供することで、複数の使用シナリオに対応することができます。
struct PixivAppNightAPIClient: Sendable {
private let client: any APIProtocol
internal init(client: Any APIProtocol) {
self.client = client
self.client =
}
public init() {
self.init(
Client(
serverURL: URL(string: "http://127.0.0.1:8080/api")!,
transport: URLSessionTransport()
)
}
}
}
上記の例では、テストの実施において柔軟性を高めるために、異なる初期化メソッドが提供されています。具体的には、テスト時にはinternal init
メソッドを使用します。これにより、テスト環境特有の設定やモックオブジェクトを注入することが可能になります。一方、通常のアプリケーション実行時にはpublic init
メソッドを使用します。この方法により、テストと実際の実行環境の間でコードの再利用性を高め、よりテストしやすい実装が可能になります。
このアプローチは、テストのための特別な条件を組み込むことなく、アプリケーションのコードベースをそのまま利用できるため、効率的なテストプロセスを実現することができます。
API
メソッドの実装
API
の各メソッドは、PixivAppNightAPIClient
内で定義され、その中でAPI
呼び出しを行います。
struct PixivAppNightAPIClient: Sendable {
private let client: any APIProtocol
// ...
func getPixivAppNightMessage(
name: String
) async throws -> String {
let response = try await client.welcomeMessage(
query: .init(name: name)
)
return try response.ok.body.json.message
}
}
この方法により、API
の利用者はSwift OpenAPI Generator
を直接使っていることを意識する必要がなくなります。
API
の使用例
PixivAppNightAPIClient
を使用して、特定のAPI
メソッドを呼び出す例は以下の通りです。
struct SomeStruct {
func fetchPixivAppNightMessage(name: String) -> String {
pixivAPIClient.getPixivAppNightMessage(
name: name
)
}
}
この構造を採用することで、利用側がAPI
の内部実装を意識することなく、API
の結果を受け取ることができます。
API
ヘッダーの注入
API
リクエストを行う際には、ヘッダーに様々な値を追加することが一般的です。Swift OpenAPI Generator
を用いたクライアント実装でも、同様にヘッダーへの値の追加が必要になります。
必要なヘッダーの定義
今回の実装では、次のようなヘッダーを追加することを考えます。
{
"App-OS": "iOS"
"Client-Id": "pixiv-app-night"
}
これらのヘッダーをAPIリクエストに注入するためには、ClientMiddleware
プロトコルの理解が重要です。
ClientMiddleware
プロトコル
ClientMiddleware
は、リクエストの前処理や後処理を行うためのプロトコルです。
public protocol ClientMiddleware: Sendable {
func intercept(
_ request: HTTPRequest,
body: HTTPBody?,
baseURL: URL,
operationID: String,
next: @Sendable (HTTPRequest, HTTPBody?, URL) async throws -> (HTTPResponse, HTTPBody?)
) async throws -> (HTTPResponse, HTTPBody?)
}
このプロトコルを使用することで、リクエストに対して必要な変更を加えた後、次の処理(next
)に進むことができます。
HeaderInjectionMiddleware
の定義
今回はHeaderInjectionMiddleware
という名前でClientMiddleware
を実装します。
struct HeaderInjectionMiddleware: ClientMiddleware {
func intercept(
_ request: HTTPTypes.HTTPRequest,
body: OpenAPIRuntime.HTTPBody?,
baseURL: URL,
operationID: String,
next: @Sendable (HTTPTypes.HTTPRequest, OpenAPIRuntime.HTTPBody?, URL) async throws -> (HTTPTypes.HTTPResponse, OpenAPIRuntime.HTTPBody?)
) async throws -> (HTTPTypes.HTTPResponse, OpenAPIRuntime.HTTPBody?) {
// ここに処理を書く
return try await next(request, body, baseURL)
}
}
このミドルウェア内で、ヘッダーへの値の追加を行います。
struct HeaderInjectionMiddleware: ClientMiddleware {
func intercept(
/* 略 */
) /* 略 */ {
var request = request
request.headerFields[/* 略 */] = /* 略 */
return try await next(request, body, baseURL)
}
}
headerFields
の省略された部分(ヘッダー名を指定する部分)にはHttpFields.Name
を指定する必要があります。
HttpFields.Name
とは?
HttpFields.Name
は、ヘッダーのフィールド名を定義するための構造体で、以下のように定義されています。
extension HTTPField {
public struct Name: Sendable {
public let rawName: String
public let canonicalName: String
// ...
}
}
この構造体を使用して、標準的なヘッダー名やカスタムヘッダー名を定義することができます。
HttpFields.Name
はデフォルトで以下のような標準的に利用されるヘッダー名がすでに用意されています。ゆえに、指定したいヘッダー名が既に定義されている場合はそちらを指定することで解決することができます。
extension HTTPField.Name {
// ...
public static var accept: Self {
.init(rawName: "Accept", canonicalName: "accept")
}
// ...
public static var cookie: Self {
.init(rawName: "Cookie", canonicalName: "cookie")
}
// ...
}
しかし今回追加したいヘッダー名であるApp-OS
およびClient-Id
はデフォルトでは存在していないです。なので、この2つのカスタムなヘッダー名は以下のようにして開発者自身で定義する必要があります。
private extension HTTPField.Name {
static let appOS = Self("App-OS")!
static let clientID = Self("Client-Id")!
}
ヘッダーの追加
HttpFields.Name
の拡張定義を行った後、ヘッダーへの値の追加を行います。指定は、.appOS
や.clientID
のようにします。
struct HeaderInjectionMiddleware: ClientMiddleware {
func intercept(
/* 略 */
) /* 略 */ {
var request = request
request.headerFields[.appOS] = "iOS"
request.headerFields[.appOS] = "pixiv-app-night"
return try await next(request, body, baseURL)
}
}
今回の例ではiOS
やpixiv-app-night
のような値をメソッド内で直接設定していますが、実際にはHeaderFactory
のような構造体を定義し値の設定は作成したFactory
経由で行うとより良い実装になるかと思います。
ClientMiddleware
の指定
作成した最後に、作成したHeaderInjectionMiddleware
をClient
に適用します。
struct PixivAppNightAPIClient: Sendable {
private let client: any APIProtocol
init() {
self.client = Client(
serverURL: URL(string: "http://127.0.0.1:8080/api")!,
transport: URLSessionTransport(),
middlewares: [ // 追加
HeaderInjectionMiddleware() // 追加
]
)
}
}
この初期化時にmiddlewares
引数にHeaderInjectionMiddleware
を指定することで、すべてのリクエストに対して自動的にヘッダーが追加されます。middlewares
は配列であるため、別のMiddleware
を作成し、指定することで複数のMiddleware
を実行することができます。
Middleware
とTransport
の役割
Swift OpenAPI Generator
を利用する際に、Client
の初期化でTransportとMiddleware
を設定していますが、それらの役割と目的を解説します。
init() {
Client(
serverURL: /* ... */
transport: URLSessionTransport(),
middlewares: [
HeaderInjectionMiddleware(),
FugaMiddleware(),
PiyoMiddleware()
]
)
}
Transport
について
Transport
は、ネットワーク操作の中心的なコンポーネントであり、HTTP
操作を実行する基本となるHTTP
ライブラリを抽象化する役割を持ちます。これにより、実際のネットワーク通信を担当し、サーバとのデータのやり取りを可能にします。
特に、使用したいHTTP
ライブラリに対応するクライアントトランスポートがまだ存在しない場合や、テストで珍しいネットワーク条件をシミュレートする必要がある場合に、Transport
の使用が重要になります。このような状況では、カスタムなTransport
の実装を検討することが推奨されます。
Middleware
について
Middleware
は、リクエストとレスポンスの処理において非常に汎用的な役割を果たします。Transport
に渡される前のリクエストと、Transport
から返された後のレスポンスを読み取り、変更することが可能です。Middleware
は、HTTP
リクエストとレスポンスを処理するものの、自身ではHTTP
の操作を行う責務を持っていません。
Middleware
は認証・ログ記録・メトリクス・トレーシング・カスタムヘッダの注入などに適しており、実際のサーバーへリクエスト時にランダムな失敗を注入したり、リトライやエラーハンドリングのロジックをテストする際にも適しています。
処理の流れ
API
リクエストを送信する際、処理は以下の順序で行われます。
-
Middleware
が順番に実行される。 -
Middleware
の処理が完了した後、Transport
が実行される。
以下の例では、HeaderInjectionMiddleware
、FugaMiddleware
、PiyoMiddleware
の順でMiddleware
が実行され、その後URLSessionTransport
が処理を引き継ぎます。
init() {
Client(
serverURL: /* ... */
transport: URLSessionTransport(), // 4番目
middlewares: [
HeaderInjectionMiddleware(), // 1番目
FugaMiddleware(), // 2番目
PiyoMiddleware() // 3番目
]
)
}
プロトコルClientMiddleware
で定義されていたメソッドintercept
では引数にnext
を取り、return
を行う際に引数で取ったnext
を指定していました。これは、next
を呼び出すことにより、次のMiddleware
の内容、もしくは、Transport
の内容を実行することを意味しています。
struct HeaderInjectionMiddleware: ClientMiddleware {
func intercept(
/* 略 */
next: @Sendable (HTTPTypes.HTTPRequest, OpenAPIRuntime.HTTPBody?, URL) async throws -> (HTTPTypes.HTTPResponse, OpenAPIRuntime.HTTPBody?)
) async throws -> (HTTPTypes.HTTPResponse, OpenAPIRuntime.HTTPBody?) {
// `next`を呼び出すことで次のMiddlewareかTransportの内容を実行する
return try await next(request, body, baseURL)
}
}
おわりに
本記事では、Swift OpenAPI Generator
の使用方法とプラクティスについて、解説しました。このツールを使うことで、iOSアプリ開発においてAPI
との連携がより容易になり、開発プロセスが効率化されます。Swift OpenAPI Generator
は、OpenAPI
仕様に対応し、自動的にクライアントおよびサーバーサイドのコードを生成する能力を持っています。
サーバ側とクライアント側の両方で、OpenAPI
ドキュメントから直接コードを生成し、Vapor
フレームワークと組み合わせることにより、iOSアプリのバックエンドとの連携を効率化しました。また、Swift OpenAPI Generator
を利用する際のプラクティスについても詳しく説明しました。これには、API
呼び出しの隠蔽や、ヘッダーの注入、Middleware
とTransport
の役割についての理解が含まれます。
このツールの導入により、開発者は煩雑なコード記述から解放され、より別の作業に集中できるようになります。さらに、自動化されたコード生成は、最新のAPI変更に迅速に対応することができ、継続的なメンテナンスとアップデートの負担を軽減します。
Swift OpenAPI Generator
は、iOSアプリ開発をより良くするツールであり、その可能性は非常に大きいと考えています。本記事を通じて、Swift OpenAPI Generator
の基本的な使用方法から実践的なプラクティスに至るまでが活用され、iOSアプリ開発の品質と生産性の向上に貢献できれば幸いです。
開発環境
- Swift compiler version info:
Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 clang-1500.1.0.2.5)
- Xcode version info:
Xcode 15.2 Build version 15C500b
-
Swift OpenAPI Generator
version info:1.2.0 (b18bece)
参考引用文献等
- https://github.com/apple/swift-openapi-generator
- https://swiftpackageindex.com/apple/swift-openapi-generator/1.2.0/tutorials/swift-openapi-generator
- https://developer.apple.com/videos/play/wwdc2023/10171/
- https://zenn.dev/kamimi01/articles/7c6f79e42c3750
- https://speakerdeck.com/kamimi/the-guide-of-swift-openapi-generator-for-beginners
Discussion