6
0

OpenAPIだってLintしたい

Posted at

この記事は、ラクス Advent Calendar 2023 の3日目の記事です。昨日は、 @choreiKotlin + Spring Boot で OpenAPI Generator 体験記①(設定内容編) でした :christmas_tree:

OpenAPIは温かみのある手書き派の @oohira です。とはいっても、やはり手書きであるがゆえの些細なミスや、(OpenAPIのスキーマとしては問題ないけど)チームで定めた独自ルールの違反をいかに減らすかは悩みの種です。

そこで、この記事では Spectral を使って1、OpenAPIもコードと同じようにLintする方法を紹介します。

セットアップ

  1. spectral コマンドのインストール
    $ npm install -g @stoplight/spectral-cli
    
    $ spectral --version
    6.11.0
    
  2. 設定ファイルの作成
    $ vi .spectral.yaml
    extends: ["spectral:oas"]
    
  3. 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
      

既存ルールの調整

Spectralが標準で提供しているOpenAPI用のルールは、次のページから確認できます。

ルールごとの Severity (error, warn, info, hint, off) を上書きすることで、検出レベルを調整することができます。

  1. 設定ファイルの変更
    $ vi .spectral.yaml
    extends: ["spectral:oas"]
    rules:
      info-contact: off
      info-description: info
      operation-tag-defined: error
    
  2. 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
    

ポイントは次の通りです。

  1. given に書かれた JSONPath で評価対象となるノードを絞り込む
  2. 絞り込んだ各ノードの field がもつ値に対して、function が成立することを検証する

givenfield で目的のフィールドを絞り込めるようにするまでが意外に大変な気がします。公式ドキュメントでも紹介されていますが、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が原則スネークケースで、パスパラメーターだけキャメルケース

functionpattern 関数を指定することで正規表現によるチェックを実現できます。なお、キャメルケースと言いつつ先頭文字と文字種ぐらいしかチェックしていない正規表現になっているので雑です。

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であるようなノード」の意味になります。また、functiontruthy 関数を指定することで、フィールドの存在チェックを実現できます。

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できて便利なんじゃないかなと思います。

参考情報

  1. vacuum というSpectral互換かつ高速性をうたったLinterも試したのですが、まだ発展途上だったのでSpectralにしました。

6
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
0