昨年末からOpenAPI 3.0について調べていました。
YAMLを書いていて、型定義を元に簡単なvalidationぐらいはやってくれるツールはないかと思って探してみたところ、
OpenApI 3.0準拠のcommitteeというrubygemで実現できることが分かりました。
準備
rails
rails new --api でプロジェクト作って、適当に./bin/rails g scaffold articleしたものを用意します。
# controller はこんな感じ(api以下に配置)。 module Api class ArticlesController < ApplicationController before_action :set_article, only: %i[show update destroy] def index articles = Article.order(created_at: :desc) render json: { status: "SUCCESS", message: "Loaded articles", data: articles } end def show render json: { status: "SUCCESS", message: "Loaded the articles", data: @article } end def create article = Article.new(article_params) if article.save render json: { status: "SUCCESS", data: article } else render json: { status: "ERROR", data: article.errors } end end
openapi
config/openapi.yml 一部抜粋。
paths: /articles/{articleId}: get: parameters: - name: articleId in: path description: ID of article of fetch required: true schema: type: integer format: int64 responses: '200': description: OK operationId: getArticleById tags: - Article description: Article by ID /articles: get: responses: '200': content: application/json: schema: type: object properties: status: type: string message: type: string data: $ref: '#/components/schemas/ArrayOfArticles' examples: example: {} description: A list of articles operationId: getArticles tags: - Articles description: A list of articles post: requestBody: content: application/json: schema: type: object required: - title - description properties: title: type: string description: type: string responses: '201': description: created operationId: createArticle tags: - Articles description: create Article components: schemas: ArrayOfArticles: type: array items: type: object properties: id: type: integer title: type: string description: type: string created_at: type: string format: date-time updated_at: type: string format: date-time servers: - url: 'http://localhost:3000/api'
committee
こんな感じでyml/jsonを読み込めばrack middlewareとして動作してくれます。
# config/application.rb schema_path = Rails.root.join("config/openapi.yml").to_s config.middleware.use Committee::Middleware::RequestValidation, schema_path: schema_path, prefix: '/api'
RequestValidation を試してみる
url path
/articles/{articleId}
GET 時の articleId は integer として定義したので
文字列だとエラーとなります。
❯❯❯ curl http://localhost:3000/api/articles/1a | jq % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 84 0 84 0 0 9333 0 --:--:-- --:--:-- --:--:-- 9333 { "id": "bad_request", message: "#/paths/~1api~1articles~1{id}/get/parameters/0/schema expected integer, but received String: 1a" }
controllerへのvalidation実装なしで、validationができてしまいました。素晴らしい!
(ですが、messageの内容が微妙です。)
request body
/articles
POST 時の parameterとしてtitleとdescriptionを定義しましたが、それぞれ必須かつ文字列としています。
descriptionなしの場合
❯❯❯ curl -X POST "http://localhost:3000/api/articles" -H "Content-Type: application/json" -d '{"title": "string"}' | jq % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 161 0 142 100 19 20285 2714 --:--:-- --:--:-- --:--:-- 23000 { "id": "bad_request", "message": "#/paths/~1articles/post/requestBody/content/application~1json/schema missing required parameters: description" }
titleをbooleanとした場合
❯❯❯ curl -X POST "http://localhost:3000/api/articles" -H "Content-Type: application/json" -d '{"title": true}' | jq % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 179 0 164 100 15 14909 1363 --:--:-- --:--:-- --:--:-- 16272 { "id": "bad_request", "message": "#/paths/~1articles/post/requestBody/content/application~1json/schema/properties/title expected string, but received TrueClass: true" }
素晴らしい。それぞれエラーとなります。
(curlは端折っていますが、HTTP/1.1 400 Bad Requestが返っています。)
(ですが、messageの内容が微妙です。)
メッセージを修正してみる
validation機能は素晴らしいのですが、message の前半がhuman readableではありません。(これはYPath?)
ソースを眺めてみると、committeeの中からopenapi_parserというのを呼んでおり、この中でvalidationをしているようです。
OpenAPIErrorのinstance変数である@referenceがこのエラーメッセージの表示箇所です。
# openapi_parser-0.6.1/lib/openapi_parser/errors.rb class ValidateError < OpenAPIError def message "#{@reference} expected #{@type}, but received #{@data.class}: #{@data}" end
やってみる
ですが、いくつかのエラーパターンがあり、ここをうまく表示するには難しそうだったので、力づくでやってみました。
以下をrails config/initializersに突っ込む。
module OpenAPIParser class OpenAPIError < StandardError def initialize(reference) @reference = customize_reference(reference) # @reference = reference end private def customize_reference(reference) # path parameter # eg: #/paths/~1articles~1{articleId}/get/parameters/0/schema return $1 if reference.match(%r|{(.+)}.+/parameters/|) # requestBody # eg: #/paths/~1articles/post/requestBody/content/application~1json/schema/properties/title return reference.split("/").last if reference.include?('/requestBody/') # other # query parameter # eg: #/paths/~1articles/get return "" end end end
結果
path error
❯❯❯ curl http://localhost:3000/api/articles/1a | jq % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 84 0 84 0 0 5600 0 --:--:-- --:--:-- --:--:-- 5600 { "id": "bad_request", "message": "articleId expected integer, but received String: 1a" }
post parameter error
❯❯❯ curl -X POST "http://localhost:3000/api/articles" -H "Content-Type: application/json" -d '{"title": "string"}' | jq % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 99 0 80 100 19 2962 703 --:--:-- --:--:-- --:--:-- 3666 { "id": "bad_request", "message": "schema missing required parameters: description" } ❯❯❯ curl -X POST "http://localhost:3000/api/articles" -H "Content-Type: application/json" -d '{"title": true}' | jq % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 99 0 84 100 15 6000 1071 --:--:-- --:--:-- --:--:-- 7071 { "id": "bad_request", "message": "title expected string, but received TrueClass: true" }
query string error(offsetを必須パラメータとした場合)
❯❯❯ curl "http://localhost:3000/api/articles" | jq % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 69 0 69 0 0 248 0 --:--:-- --:--:-- --:--:-- 248 { "id": "bad_request", "message": " missing required parameters: offset" }
まとめ
committee のRequestValidationを試してみました。
YAMLだけで基本的な型チェックをしてくれるのはとても素晴らしいです。
ですが、error responseをもう少し柔軟にカスタマイズできる機能は要望がありそう、ということで無理やりやってみました。
また、今回じっくりみてませんが、ResonseValidationもあり、テストで利用できるのは大きいので
ここはガッツリ使えるかと思います。
あと、別件ですが、正直OpenApiのYAML書くのが辛いなぁという印象。