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

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

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

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

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

## dry-validationとは
[dry-validation](https://github.com/dry-rb/dry-validation)は[dry-rb](https://github.com/dry-rb)というコンポーネント指向なライブラリ群のひとつで、端的に言うとバリデーションを行うことに特化したモジュールだ

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

```ruby
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はバリデーション・ロジック自体の機能しか提供しないので、実際にエラーコードを発行する処理は自前でやらねばらない。

```ruby
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を返す等）は何度も同じコードを使いまわしていることに気づく。こうしたリクエストハンドラの中で何度も登場するような処理は、ヘルパにするのが吉だ。

```ruby
  helpers do
    def validates(&block)
      schema = Dry::Validation.Schema(&block)
      validation = schema.call(params)
      halt 422 if validation.failure?
    end
  end
```
このようなヘルパを実装することで、リクエストハンドラの中のコードはスキーマの実装だけでよくなる。

```ruby
get '/hello' do
  validates do
    required(:id).filled(:str?)
  end
end
```
若干DSL色が強めだが、ヘルパを使うことでDRYになったことが分かる。

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

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

```ruby
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`オプション](https://github.com/IzumiSy/sinatra-validation#raise)で例外を投げるようにしたり、バリデーションエラーを返り値で受け取れるようにする[`silent`オプション](https://github.com/IzumiSy/sinatra-validation#silent)など様々な機能がある。

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