10
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

個人開発Advent Calendar 2017

Day 4

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

Posted at

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を出して更新を促すという処置もできたかもしれない。

10
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?