OpenAPIドキュメントを書いて作って公開する
はじめに
これまでいくつかのプラットホームで RESTful な WebAPI サーバーを作る記事を書いてきました。そして、WebAPI の仕様書としては、デファクトスタンダード的な位置づけの OpenAPI の仕様についても記事を書いてきました。
今回は、OpenAPI 仕様に基づいたドキュメントを書いて、ブラウザで表示可能なファイルとして作り、WebAPI サーバに同梱して公開するという内容になります。
書く
これまでの WebAPI サーバと同じお題で作ります(が、公開リポジトリでは未実装の部分も拡張します)。openapi.yaml とか openapi.json とかチラ見したことがあるのですが、あんな長い呪文を書くのは厳しいです。
環境
もうすっかり VSCode 無しでは生きていけない状態なので、VSCode 上で OpenAPIドキュメントを書くための環境を作ります。ネタバレ的には、作る で ReDocly CLI を使うこともあり、ReDocly OpenAPI 拡張をインストールします。
これで、コードアシストや (お節介な) 警告を出してくれるので、安心して進められそうです。
ビジュアルな入力も目指して頑張って開発してくれていますが、将来に期待。ということで素通りします。
構成
ツールを使って書きやすくはなったのですが、警告の洗礼を受けながら、あんな長い呪文を書くのは心が折れてしまいます。OpenAPI 仕様では、様々な箇所で Referenceオブジェクト を使うことができます。
これを使って、分割統治を試行錯誤した結果、以下のような構成にします。
$ tree openapi
openapi
├── customers
│ ├── collection.yaml
│ ├── identified.yaml
│ ├── key.yaml
│ ├── query.yaml
│ └── resource.yaml
├── main.yaml
書いた
トップレベル
トップレベルの yaml ファイルは、よく見るものと似ていますが、paths の Path Itemオブジェクト をまるっと外に出します。
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
まぁ、だいたい、リソースの集合に対しての呼び出しか、特定のリソースを指定した呼び出しになりますよね。
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
登録とクエリの場合は、入出力でリソースのスキーマを定義するんですが、そこも外出しにします。クエリの場合は、加えて、クエリパラメータも定義するんですが、ここはここしか使わないのですが、全体のバランスをとるために外出しにしました。
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
結果、外出しされた残りとなる末端のパスパラメータ、クエリパラメータ、リクエスト/レスポンスのボディ部を定義します。
- name: customerId
in: path
description: 顧客の識別子
schema:
type: integer
format: int64
- 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
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 で $ref
と required
を混ぜて定義することにしました。
作る
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 を使うと簡単に実現できます。
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 で公開しました。
Discussion