Ruby
Sinatra

sinatraのリクエストパラメータ・バリデーションにdry-validationを使ってみよう

More than 1 year has passed since last update.

SinatraでAPIサーバをつくるとき、いわゆるStrongParameterのようなリクエストパラメータのバリデーションをどのように作るだろうか?

筆者は2年前くらいから、Sinatraでチャット用のAPIサーバをちびちびと開発していて、ずっとsinatra-paramというバリデーション・ヘルパのモジュールを使っていた。しばらくは満足していたのだが、残念なことにこのGemはあまり更新頻度が高くなく、2017年のSinatraの2.x系アップグレードには全く持って追従していないことが分かった。パフォーマンスの観点などからできるだけ最新のSinatraを使いたいので、こうしたGemの依存でアップグレードに追従できなくなるのはやはり避けたいところ。というわけで、この気にsinatra-paramを使うのをやめ、dry-validationというコンポーネント化されたバリデーション・モジュールに乗り換えることにした。

そもそも、dry-validationのような特定の機能に絞られたモジュールを組み合わせて使うことは「疎結合性」の観点でアドバンテージがある。sinatra-paramのようにフレームワークの実装に乗っかったモジュールの場合には、フレームワークの変更によって、モジュールが動かなくなる可能性がある1。言い換えれば、今回のように特定のモジュールが他のなにかに依存していることによって、コードベースを弄らずには、モジュールを更新できない状態になりえるということだ。数百行のソースコードであればいいかもしれないが、これが何千万行のエンタープライズ向けアプリケーションともなれば、ちょっとは辛くもなってくるというものだ。

dry-validationとは

dry-validationdry-rbというコンポーネント指向なライブラリ群のひとつで、端的に言うとバリデーションを行うことに特化したモジュールだ

Dry::Validation.Schemaにブロックを渡すことで、バリデーションルールを定義できる。

require 'dry-validation'

schema = Dry::Validation.Schema do
  required(:email).filled(:str?)
  required(:age).filled(:int?, gt?: 18)
end

schema.call(email: nil, age: 19).messages
# { :email => ["must be filled"] }

Sinatraで使ってみる

これをSinatraで使うには、リクエストハンドラの中でうけとれるparamsを引数にしてバリデーションよ呼び出すだけでよい。dry-validationはバリデーション・ロジック自体の機能しか提供しないので、実際にエラーコードを発行する処理は自前でやらねばらない。

get '/hello' do
  # バリデーションスキーマを定義
  schema = Dry::Validation.Schema do
    required(:id).filled(:str?)
  end

  # バリデーションを実行
  validation = schema.call(params)

  # 以下のふたつが使えるので、どちらかを使う
  # - validation.failure?
  # - validation.success?
  # バリデーションが失敗していたら422(Unproceccessable Entity)を返す
  halt 422 if validation.failure?

  ...
end

ヘルパにしてみる

上の実装によってバリデーションは実装できたが、しばらく実装を続けていると、スキーマを定義する以外の箇所(バリデーションが失敗していたら422を返す等)は何度も同じコードを使いまわしていることに気づく。こうしたリクエストハンドラの中で何度も登場するような処理は、ヘルパにするのが吉だ。

  helpers do
    def validates(&block)
      schema = Dry::Validation.Schema(&block)
      validation = schema.call(params)
      halt 422 if validation.failure?
    end
  end

このようなヘルパを実装することで、リクエストハンドラの中のコードはスキーマの実装だけでよくなる。

get '/hello' do
  validates do
    required(:id).filled(:str?)
  end
end

若干DSL色が強めだが、ヘルパを使うことでDRYになったことが分かる。

sinatra-validation

原点回帰してしまう感はあるが、sinatra-validationというものがある。これは、ここまで実装したdry-validationをより便利に使うためのSinatraのヘルパ・モジュールを提供してくれるGemだ。これをつかうことで、上で実装してきたヘルパ+αで便利なオプションなどを使うことができる。

アプリケーションにSinatra::Validationを登録するだけでvalidatesヘルパがリクエストハンドラの中で使えるようになる。

class Application < Sinatra::Base
  configure do
    register Sinatra::Validation
  end

  get '/basic' do
    validates do
      required("name").filled(:str?)
      required("age").filled(:str?)
    end
  end
end

その他にも、raiseオプションで例外を投げるようにしたり、バリデーションエラーを返り値で受け取れるようにするsilentオプションなど様々な機能がある。

dry-validationはdry-rbという、特定の機能にフォーカスを絞った小さなライブラリ・コレクションのうちのひとつで、それ以外にも様々な便利ライブラリが充実している。今回はバリデーションの観点でSinatraとの組み合わせを記事にしたが、他のライブラリもいろいろと見てみると良いかと思われる。


  1. sinatra-paramはgemspecでSinatraの依存バージョンが固定されていただけで、実際に内部の実装が1系でしか動かないものであるかどうかは分からないので、PRを出して更新を促すという処置もできたかもしれない。