この記事は、ラクス Advent Calendar 2023 の3日目の記事です。昨日は、 @chorei の Kotlin + Spring Boot で OpenAPI Generator 体験記①(設定内容編) でした
OpenAPIは温かみのある手書き派の @oohira です。とはいっても、やはり手書きであるがゆえの些細なミスや、(OpenAPIのスキーマとしては問題ないけど)チームで定めた独自ルールの違反をいかに減らすかは悩みの種です。
そこで、この記事では Spectral を使って1、OpenAPIもコードと同じようにLintする方法を紹介します。
セットアップ
- spectral コマンドのインストール
$ npm install -g @stoplight/spectral-cli $ spectral --version 6.11.0
- 設定ファイルの作成
$ vi .spectral.yaml extends: ["spectral:oas"]
- Lintの実行
$ spectral lint petstore.yaml /work/petstore.yaml 2:6 warning info-contact Info object must have "contact" object. info 2:6 warning info-description Info "description" must be present and non-empty string. info 11:9 warning operation-description Operation "description" must be present and non-empty string. paths./pets.get 15:11 warning operation-tag-defined Operation tags must be defined in global tags. paths./pets.get.tags[0] 43:10 warning operation-description Operation "description" must be present and non-empty string. paths./pets.post 47:11 warning operation-tag-defined Operation tags must be defined in global tags. paths./pets.post.tags[0] 58:9 warning operation-description Operation "description" must be present and non-empty string. paths./pets/{petId}.get 62:11 warning operation-tag-defined Operation tags must be defined in global tags. paths./pets/{petId}.get.tags[0] ✖ 8 problems (0 errors, 8 warnings, 0 infos, 0 hints)
- 環境を汚したくない場合はDockerでも可
$ docker run --rm -it -v $(pwd):/work -w /work stoplight/spectral lint petstore.yaml
- 環境を汚したくない場合はDockerでも可
既存ルールの調整
Spectralが標準で提供しているOpenAPI用のルールは、次のページから確認できます。
ルールごとの Severity (error, warn, info, hint, off
) を上書きすることで、検出レベルを調整することができます。
- 設定ファイルの変更
$ vi .spectral.yaml extends: ["spectral:oas"] rules: info-contact: off info-description: info operation-tag-defined: error
- Lintを実行
$ spectral lint petstore.yaml /work/petstore.yaml 2:6 information info-description Info "description" must be present and non-empty string. info 11:9 warning operation-description Operation "description" must be present and non-empty string. paths./pets.get 15:11 error operation-tag-defined Operation tags must be defined in global tags. paths./pets.get.tags[0] 43:10 warning operation-description Operation "description" must be present and non-empty string. paths./pets.post 47:11 error operation-tag-defined Operation tags must be defined in global tags. paths./pets.post.tags[0] 58:9 warning operation-description Operation "description" must be present and non-empty string. paths./pets/{petId}.get 62:11 error operation-tag-defined Operation tags must be defined in global tags. paths./pets/{petId}.get.tags[0] ✖ 7 problems (3 errors, 3 warnings, 1 info, 0 hints)
カスタムルールの作り方
Spectralでは、チーム独自の規約などをカスタムルールとして定義してチェックさせることができます。OpenAPIの仕様違反であれば他にもツールはあると思いますが、チームのAPI規約をルール化できるという点がSpectralのうれしいところです。
詳細な仕様は、ドキュメント を参照してもらうとして、簡単なルールの作り方だけ紹介します。ここでは、operationId
がスネークケースであることをLintしたいとします。
- 対象となるOpenAPI
paths: /pets: get: summary: List all pets operationId: listPets # キャメルケースなのでNG
- カスタムルール
rules: operation-id-snake-case: message: "operationIdはスネークケース. {{value}}" given: $.paths.*.* severity: error then: field: operationId function: casing functionOptions: type: snake
- 実行イメージ
$ spectral lint petstore.yaml ... 13:20 error operation-id-snake-case operationIdはスネークケース. listPets paths./pets.get.operationId
ポイントは次の通りです。
-
given
に書かれた JSONPath で評価対象となるノードを絞り込む - 絞り込んだ各ノードの
field
がもつ値に対して、function
が成立することを検証する-
function
には組み込みで提供される コア関数 のほかに、JavaScriptで書いたカスタム関数 を使うこともできます
-
given
と field
で目的のフィールドを絞り込めるようにするまでが意外に大変な気がします。公式ドキュメントでも紹介されていますが、JSONPath Online Evaluatorなどで確認しながら進めるとよいでしょう。
カスタムルールの例
いくつかのサンプルを紹介して、詳細な説明に代えたいと思います。
プロパティ名がスネークケース
field
に @key
を指定することで properties オブジェクトの各キーを対象にできます。また、properties 要素は paths 以下や components 以下などいくつかの場所で使われるため、 $..
を使って階層を固定せずにマッチングさせます。
Pet:
type: object
properties:
id: # ここに対するLint
type: integer
name: # ここに対するLint
type: string
rules:
properties-snake-case:
message: "propertiesのキーはスネークケース"
severity: error
given: "$..properties"
then:
field: "@key"
function: casing
functionOptions:
type: snake
URLが原則スネークケースで、パスパラメーターだけキャメルケース
function
に pattern
関数を指定することで正規表現によるチェックを実現できます。なお、キャメルケースと言いつつ先頭文字と文字種ぐらいしかチェックしていない正規表現になっているので雑です。
paths:
/pets/{petId}: # ここに対するLint
get:
summary: Info for a specific pet
rules:
paths-snake-case-except-params:
message: "URLはスネークケース(パラメーターのみキャメルケース)"
severity: error
given: "$.paths"
then:
field: "@key"
function: pattern
functionOptions:
match: "^([/a-z0-9_]|{[a-z][a-zA-Z0-9]*})*$"
array型のプロパティにはmaxItemsも定義
given
がトリッキーですが、*[?(フィルタ)]
でフィルタを満たす任意のノードになります。ここでは、@.type=='array'
すなわち「typeプロパティの値がarrayであるようなノード」の意味になります。また、function
に truthy
関数を指定することで、フィールドの存在チェックを実現できます。
Pets:
type: array
maxItems: 100 # ここに対するLint
items:
$ref: "#/components/schemas/Pet"
rules:
array-max-items:
message: "array型にはmaxItemsも必ず定義する"
severity: error
given: "$..*[?(@.type=='array')]"
then:
field: maxItems
function: truthy
CIへの組み込み
最後に、自動的にLintが実行されるようCIへ組み込んでおくとよいでしょう。npmパッケージ 以外にも、Dockerイメージ や GitHub Actions も公開されているので、好みの方法で簡単に組み込めると思います。
まとめ
この記事では、OpenAPIをLintするためのツールとしてSpectralを紹介しました。OpenAPIの手書き派が少ないのか、あまり情報が出回っていない気がしますが、プロジェクトの規約をうまくカスタムルール化できれば手軽にLintできて便利なんじゃないかなと思います。