OpenAPIKit を使って Swift で OpenAPI ドキュメントをバリデーションする
OpenAPIKit というライブラリを使ってみたので、使い方やできることを紹介します。
OpenAPIKit とは
mattpolzin さんが作成しています。
メインの機能は OpenAPI ドキュメントとそのコンポーネントへのエンコードとデコードです。また OpenAPI ドキュメントのバリデーションも行うことができます。特徴はそれを Swift で行うことができることです。
OpenAPI のドキュメントは、yaml または json で書きます。
このライブラリを使うと、そのドキュメントをデコードして Swift のオブジェクトに変換することができます。逆も可能で、Swift のオブジェクトから yaml や json の文字列に変換することもできます。
この記事ではバリデーションに焦点を当てます。
ちなみにこのライブラリは、WWDC23 のセッションで発表された Swift OpenAPI Generator の内部で yaml や json のドキュメントをデコードするために使用されています。
Swift OpenAPI Generator のドキュメントに「OpenAPI ドキュメントのバリデーションには、Swift OpenAPI Generator も利用している OpenAPIKit というライブラリも使うのもいいよ」(意訳)と書かれていたので気になって調べてみました。
OpenAPIKit で OpenAPI ドキュメントをバリデーションする
OpenAPI ドキュメントのバリデーションには様々なツールがありますが、今回は OpenAPIKit で OpenAPI ドキュメントをバリデーションしてみました。
といってもステップ自体はシンプルです。
OpenAPIKit でデコードし、用意されているバリデーションメソッドを使うだけです。
ちなみに今回は OpenAPI のドキュメントは yaml ファイルを想定しますが、json ファイルの場合は、json 用のデコードをすれば同じように OpenAPIKit でバリデーションできます。
1. OpenAPI ドキュメントを準備する
OpenAPI ドキュメントをバリデーションするので当然ではありますが、準備してください。
ここでは yaml や json の書き方は説明しません。🙏🏻
2. OpenAPIKit と Yams(yaml の場合のみ) を Dependencies に追加する
まずは今回使用するライブラリを Depedencies に追加します。
yaml ファイルを使用している場合は、Yams も一緒に追加します。これは yaml ファイルをデコードするために使用します。json の場合は Foundation
が提供するクラスを使用するので、それ用のライブラリの追加はここでは不要です。
- OpenAPIKit
- Yams(yaml の場合のみ)
3. OpenAPI のドキュメントを読み込む
OpenAPI ドキュメントを読み込んで、String
型に変換します。
let url = Bundle.main.url(forResource: "openapi", withExtension: "yaml")
guard let fileURL = url,
let fileContents = try? String(contentsOf: fileURL, encoding: .utf8)
else { fatalError("cannot read file") }
4. OpenAPI ドキュメントを Swift オブジェクトにデコード
次にデコードをします。OpenAPIKit が用意している OpenAPI.Document
という型に変換します。
前述したように json ファイルをデコードする場合は、Foundation
が提供する JSONDecoder
を使用すれば良いのですが、yaml の場合は Yams のライブラリが提供するデコード用のクラスを使用します。
まずは必要なライブラリを全て import します。
import Foundation
import OpenAPIKit
import Yams
yaml のデコードは以下のように実装します。例外を投げるので、do-catch
で囲うなどして使用してください。
do {
let decoder = YAMLDecoder()
let openAPIDoc = try decoder.decode(OpenAPI.Document.self, from: fileContents)
} catch let error {
// OpenAPI.Error でラップするとエラーが読みやすくなる
print(OpenAPI.Error(from: error).localizedDescription)
}
OpenAPI ドキュメントのバリデーションという観点に焦点を当てると、不正な書き方をしている場合はこのデコード時点で検知することができます。
例えば必須の項目である、info
が存在しなかった場合、以下のようなデコードエラーが発生します。
dataCorrupted(Swift.DecodingError.Context(codingPath: [], debugDescription: "The given data was not valid YAML.", underlyingError: Optional(Expected to find `info` key in the root Document object but it is missing.)))
ちなみに、OpenAPI.Error でラップすると以下のように少しエラーが読みやすくなります。
Expected to find `info` key in the root Document object but it is missing.
5. OpenAPIKit のバリデーションメソッドを使う
デコード時点でかなりバリデーションはされますが、OpenAPI の定義を超えた独自のバリデーションをしたい場合もあると思います。
そういった場合に、OpenAPIKit が提供するバリデーションメソッドを使うことができます。
詳細なドキュメントは以下です。
ちなみに OpenAPIKit を内部で使用している Swift OpenAPI Generator は、OpenAPIKit のバリデーションメソッドは使用しておらず、主にデコードの目的で使用していました。
バリデーションの実装
実装自体はとても簡単で、validate
メソッドを呼び出すだけです。
// 省略・・・
try openAPIDoc.validate()
ではこの validate
メソッドが何をしているのか、独自のバリデーションを追加したい場合はどうすれば良いのか見ていきます。
このメソッドについては以下3種類のバリデーションを覚えておくと良いかと思います。ちなみに最初の2つに関しては、Validation+Builtins.swift
に全て実装されています。
-
validate
メソッドにデフォルトで組み込まれているバリデーション - デフォルトではないが追加可能なバリデーション
- 自分でカスタムして追加できるバリデーション
では一つずつ見ていきます。
1. validate
メソッドにデフォルトで組み込まれているバリデーション
まず現時点(2023/7/5)で validate
メソッドにデフォルトで組み込まれているバリデーションを紹介します。
バリデーション | 概要 |
---|---|
operationsContainResponses |
全ての Operations が少なくとも一つの response を保持していること |
documentTagNamesAreUnique |
全ての Tags が一意な名前であること |
pathItemParametersAreUnique |
全ての Path Items が重複した parameter を保持していないこと |
operationParametersAreUnique |
全ての Operations が重複した parameter を保持していないこと |
operationIdsAreUnique |
全ての Operation Id がドキュメント全体で一意であること |
schemaReferencesAreValid |
全ての JSONScheme の参照がドキュメントの components の辞書の中に存在すること |
responseReferencesAreValid |
全ての Response の参照がドキュメントの components の辞書の中に存在すること |
parameterReferencesAreValid |
全ての parameter の参照がドキュメントの components の辞書の中に存在すること |
exampleReferencesAreValid |
全ての example の参照がドキュメントの components の辞書の中に存在すること |
requestReferencesAreValid |
全ての request の参照がドキュメントの components の辞書の中に存在すること |
headerReferencesAreValid |
全ての header の参照がドキュメントの components の辞書の中に存在すること |
2. デフォルトではないが追加可能なバリデーション
次にデフォルトでは組み込まれていないですが、追加可能なバリデーションです。
ここでは簡単にしか紹介しませんが、ドキュメントコメントにはなぜこれらがデフォルトのバリデーションに組み込まれていないかの理由も書いてあるので、気になる方は読んでみると OpenAPI 仕様の理解が進むのでおすすめです。
バリデーション | 概要 |
---|---|
documentContainsPaths |
PathItem の中に少なくとも一つの path があること |
pathsContainOperations |
全ての PathItems が少なくとも一つの operation を保持していること |
schemaComponentsAreDefined |
全ての JSONSchemas が少なくとも一つの特性が定義されていること |
pathParametersAreDefined |
エンドポイントのパスが変数を含む場合("{variable}")、各エンドポイントの PathItem または Operation に対応するパラメータ項目が存在すること |
serverVariablesAreDefined |
全ての server オブジェクトが、URL テンプレートにあるすべての変数を定義していること |
以下のように使用します。チェーンで繋げて実装できるのが便利ですね。
// 省略・・・
try openAPIDoc.validate(using: Validator()
.validating(.documentContainsPaths)
.validating(.pathsContainOperations)
.validating(.operationsContainResponses)
.validating(.schemaComponentsAreDefined)
.validating(.pathParametersAreDefined)
.validating(.serverVariablesAreDefined)
3. 自分でカスタムして追加できるバリデーション
最後に自分で独自のバリデーションを作成してみます。
詳しいドキュメントは以下です。
独自のバリデーションを作成するためには、2つの方法があります。
-
validation()
ヘルパーメソッドを使用する -
Validation
型の値を作成する
どちらも使い方は似ているところが多いので、今回は後者の実装について説明します。
Validation
構造体には以下の2種類の初期化メソッドが用意されています。
こちらは複数のエラーを返すことができる初期化メソッドです。
init(
check validate: @escaping (ValidationContext<Subject>) -> [ValidationError],
when predicate: @escaping (ValidationContext<Subject>) -> Bool = { _ in true }
)
こちらは単一のエラーを作成するための初期化メソッドです。
init(
description: String,
check validate: @escaping (ValidationContext<Subject>) -> Bool,
when predicate: @escaping (ValidationContext<Subject>) -> Bool = { _ in true }
)
それでは後者の初期化メソッドを使用して、「servers にローカル環境向きのエンドポイントが定義されていた場合に、servers に2つ以上定義されていること」をチェックするバリデーションを作成してみます。
ローカル環境向きのエンドポイントがあるならば、当然本番環境や開発環境向きのエンドポイントも定義しておいてほしいというケースを想定します。
OpenAPI ドキュメントで見ると、
理想はこうなっていて欲しい↓
# 省略・・・
servers:
- url: http://localhost:3000/api
description: Local server
- url: https://example.com/api
description: Prod server
# 省略・・・
ですが、以下のように本番環境向きのエンドポイントを定義し忘れている場合、検知してもらうようにします。
# 省略・・・
servers:
- url: http://localhost:3000/api
description: Local server
# 省略・・・
実装は以下のようになります。
引数は以下の3つで、簡単に言うと、「理想の状態は何で」「どんなバリデーションをするか」「どんな条件でするか」です。
-
description
: validate メソッドによって記述された正しい状態 -
check
: バリデーションする関数。バリデーションが成功した場合は true を返し、失敗した場合は false を返す -
when
: このバリデータが指定した値に対して実行される場合に true を返す関数。
let serverEnvValidation = Validation(
description: "At least two servers are specified if one of them is the local server.",
check: \[OpenAPI.Server].count >= 2,
when: { context in
for server in context.subject {
if server.urlTemplate.absoluteString.contains("localhost") {
return true
}
}
return false
}
)
try openAPIDoc.validate(using: Validator()
.validating(serverEnvValidation)
)
ここでは servers の URL の値に localhost
が含まれていた場合に、servers の定義が2つ以上あるかをチェックしています。
定義が不足している状態で実際に実行してみると、以下のようなエラーが発生していることが確認できます。
Failed to satisfy: At least two servers are specified if one of them is the test server. at path: .servers
おわりに
OpenAPI ドキュメントは iOS エンジニアだけに必要なドキュメントというわけでないため、Swift でバリデーションをしたいというユースケースは少ないかもしれません。
ですが例えば個人開発アプリのような小さいアプリで公開されている API を使う場合、Swift に慣れているエンジニアが多いなどといった状況のとき(多くのアプリ開発ではそういう状況は少なそうですが)など、役に立つことはあるんじゃないかなと思いました。
私は前者の個人開発アプリで使用しています。
ちなみに私がこのライブラリを調べたきっかけは前述したように Swift OpenAPI Generator の内部で使用されていたからです。
Swift OpenAPI Generator が使用しているのは今回取り上げたバリデーション部分ではなくデコードがメインなのですが、Swift OpenAPI Generator にとっては重要なライブラリなのではないかと思い、ついでにバリデーション機能も使ってみるかと思い本記事を書くに至りました。
以上、ここまで読んでいただきありがとうございました!🤗
Discussion