📝

OpenAPIドキュメントを書いて作って公開する

2023/07/18に公開

はじめに

これまでいくつかのプラットホームで RESTful な WebAPI サーバーを作る記事を書いてきました。そして、WebAPI の仕様書としては、デファクトスタンダード的な位置づけの OpenAPI の仕様についても記事を書いてきました。

今回は、OpenAPI 仕様に基づいたドキュメントを書いて、ブラウザで表示可能なファイルとして作り、WebAPI サーバに同梱して公開するという内容になります。

書く

これまでの WebAPI サーバと同じお題で作ります(が、公開リポジトリでは未実装の部分も拡張します)。openapi.yaml とか openapi.json とかチラ見したことがあるのですが、あんな長い呪文を書くのは厳しいです。

環境

もうすっかり VSCode 無しでは生きていけない状態なので、VSCode 上で OpenAPIドキュメントを書くための環境を作ります。ネタバレ的には、作る で ReDocly CLI を使うこともあり、ReDocly OpenAPI 拡張をインストールします。

https://marketplace.visualstudio.com/items?itemName=Redocly.openapi-vs-code

これで、コードアシストや (お節介な) 警告を出してくれるので、安心して進められそうです。
ビジュアルな入力も目指して頑張って開発してくれていますが、将来に期待。ということで素通りします。

構成

ツールを使って書きやすくはなったのですが、警告の洗礼を受けながら、あんな長い呪文を書くのは心が折れてしまいます。OpenAPI 仕様では、様々な箇所で Referenceオブジェクト を使うことができます。

これを使って、分割統治を試行錯誤した結果、以下のような構成にします。

$ tree openapi
openapi
├── customers
│   ├── collection.yaml
│   ├── identified.yaml
│   ├── key.yaml
│   ├── query.yaml
│   └── resource.yaml
├── main.yaml

書いた

トップレベル

トップレベルの yaml ファイルは、よく見るものと似ていますが、paths の Path Itemオブジェクト をまるっと外に出します。

main.yaml
openapi: 3.1.0

info:
  title: sample
  version: 1.0.0
  description: Veleta で自動生成した API Server の Golang 版のサンプル
  license:
    name: Creative Commons Attribution Non Commercial Share Alike 4.0 International
    identifier: CC-BY-NC-SA-4.0
    url: https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode

servers:
  - url: "http://localhost:3000"
    description: 内部の API Server 本体

paths:
  /customers:
    $ref: customers/collection.yaml
  /customers/{customerId}:
    $ref: customers/identified.yaml
  /products:
    $ref: products/collection.yaml
  /products/{productId}:
    $ref: products/identified.yaml
  /orders:
    $ref: orders/collection.yaml
  /orders/{orderId}:
    $ref: orders/identified.yaml      

components:
  securitySchemes:
    basicAuth:
      type: http
      scheme: basic

security:
  - basicAuth: []

Path Item

まぁ、だいたい、リソースの集合に対しての呼び出しか、特定のリソースを指定した呼び出しになりますよね。

collection.yaml
post:
  operationId: postCustomer
  summary: 顧客の登録
  description: 顧客を新規登録する
  requestBody:
    content:
      application/json:
        schema:
          $ref: resource.yaml
          required:
            - customerId
            - name
            - address
  responses:
    "200":
      description: OK
      content:
        application/json:
          schema:
            $ref: resource.yaml
    "400":
      description: Bad Request
    "404":
      description: Not Found
    "409":
      description: Conflict
    "500":
      description: Internal Server Error

get:
  operationId: queryCustomers
  summary: 顧客の抽出
  description: 顧客を検索する
  parameters:
    $ref: "./query.yaml"
  responses:
    "200":
      description: OK
      content:
        application/json:
          schema:
            type: array
            items:
              $ref: resource.yaml
    "400":
      description: Bad Request
    "404":
      description: Not Found
    "500":
      description: Internal Server Error

登録とクエリの場合は、入出力でリソースのスキーマを定義するんですが、そこも外出しにします。クエリの場合は、加えて、クエリパラメータも定義するんですが、ここはここしか使わないのですが、全体のバランスをとるために外出しにしました。

identified.yaml
get:
  operationId: getCustomer
  summary: 顧客の取得
  description: 識別子で指定した顧客を取得する
  parameters:
    $ref: key.yaml
  responses:
    "200":
      description: OK
      content:
        application/json:
          schema:
            $ref: resource.yaml
    "400":
      description: Bad Request
    "404":
      description: Not Found
    "500":
      description: Internal Server Error
        
put:
  operationId: putCustomer
  summary: 顧客の更新
  description: 識別子で指定した顧客を更新する
  parameters:
    $ref: key.yaml
  requestBody:
    content:
      application/json:
        schema:
          $ref: resource.yaml       
  responses:
    "200":
      description: OK
      content:
        application/json:
          schema:
            $ref: resource.yaml
    "400":
      description: Bad Request
    "404":
      description: Not Found
    "500":
      description: Internal Server Error
        
delete:
  operationId: deleteCustomer
  summary: 顧客の削除
  description: 識別子で指定した顧客を削除する
  parameters:
    $ref: key.yaml
  responses:
    "200":
      description: OK
    "400":
      description: Bad Request
    "404":
      description: Not Found
    "500":
      description: Internal Server Error

リソースのキー指定はパスパラメータになるのですが、ここも外出しにします。

Schema

結果、外出しされた残りとなる末端のパスパラメータ、クエリパラメータ、リクエスト/レスポンスのボディ部を定義します。

key.yaml
- name: customerId
  in: path
  description: 顧客の識別子
  schema:
    type: integer
    format: int64
query.yaml
- name: name
  in: query
  description: 顧客の氏名
  schema:
    type: array
    items: 
      type: string
- name: address
  in: query
  description: 顧客の住所
  schema:
    type: array
    items: 
      type: string
- name: limit
  in: query
  description: 最大件数
  schema:
    type: integer
    minimum: 1
    maximum: 100
    default: 100
- name: customerId
  in: query
  description: 検索済みの顧客の識別子
  schema:
    type: integer
    format: int64
resource.yaml
type: object
properties: 
  customerId:
    title: 顧客ID
    description: 顧客の識別子
    type: integer
    format: int64
  name:
    title: 氏名
    description: 顧客の氏名
    type: string
    minLength: 1
    maxLength: 16
  address:
    title: 住所
    description: 顧客の住所
    type: string
    minLength: 1
    maxLength: 128

と、こんな感じであれば、それぞれのファイルを書く際に知っていないければいけない情報が限定されるので、これなら書けそうだなと思ってもらえたのではないでしょうか。

工夫した点としては、同じリソースのスキーマでも、必須条件のバリデーションは、登録なのか、更新なのか、参照なのかによって異なりますので、リソース側では指定できません。このため、上記では、アクションが決定する Operationオブジェクト 内の Schema で $refrequired を混ぜて定義することにしました。

作る

OpenAPI で作る仕様書と言えば Swagger UI なんでしょうけど、組織の内部で小さく使う API Server の仕様書のために Web サーバ用意するの?とかあると思うので、HTMLファイル1本の漢らしい ReDocly CLI で作ります。

環境

ReDocly CLI は、Node.js で動くツールなので、以下のようにインストールします。

$ npm i -g @redocly/cli@latest
$ redocly --version
1.0.0-beta.127

作った

まぁ、呼ぶだけです。

$ redocly build-docs ./openapi/main.yaml --output=./server/spec/openapi.html --theme.openapi.disableSearch
Prerendering docs
🎉 bundled successfully in: ./server/spec/openapi.html (163 KiB) [⏱ 17ms].

出来上がりが見たい方、github 上の openapi.html です。いかがでしょうか。

公開する

仕様書を納品ドキュメントに収めておいても、どこかへ行ってしまうだけのような気もしますので、WebAPI サーバにホストさせてみます。

環境

今回も Golang で行きます。

$ go version
go version go1.19.9 linux/amd64
$ go mod init github.com/take0a/openapi-sample
go: creating new go.mod: module github.com/take0a/openapi-sample
go: to add module requirements and sums:
        go mod tidy
$ go mod tidy

埋め込む

go:embed を使うと簡単に実現できます。

main.go
package main

import (
	"embed"
	"net/http"
)

//go:embed spec
var spec embed.FS

func main() {
	http.Handle("/spec/", http.FileServer(http.FS(spec)))
	http.ListenAndServe(":3000", nil)
}

これだけです。

おわりに

いかがでしたでしょうか。簡単そうだなと思って頂けたら、この記事は成功です。
今回の記事で使用したリソースを github で公開しました。

https://github.com/take0a/openapi-sample

GitHubで編集を提案
株式会社ROBONの技術ブログ

Discussion