Rails4のバリデータをテストまでしっかり書いた備忘録
はじめに
Rails4には様々なバリデータが用意されていますが、プロダクトを実装する過程で自作のバリデータを定義して使いたくなることが稀によくあります。稀によくある事なので、その都度使い方を調べていていますが、バリデータの実装からロケールファイルの定義、テストの書き方までまとまっているページがないので備忘録程度にまとめておきます。
バリデータ
今回作るバリデータについて
バリデーション対象のカラムの内容が hoge であるかチェックするHogeValidatorを作成します。バリデーションのオプションで hoge が連続する回数を指定できるようにします。
# text が `hoge` であることのバリデーション validates :text, :hoge => {:count => 1} # text が `hogehoge` であることのバリデーション validates :text, :hoge => {:count => 2}
こんな感じで使えるバリデータを作ります。
バリデータの定義
`app/validators` フォルダに `hoge_validator.rb` を作ります。
class HogeValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) unless ('hoge' * options[:count]) == value record.errors.add(attribute, :hoge, options) end end end
テスト
`spec/validators/` フォルダに `hoge_validator_spec.rb` を作ります。
require 'spec_helper' describe HogeValidator do let :validator do HogeValidator.new({:attributes => [attribute]}, :count => 1) end let :errors do ActiveModel::Errors.new(mock_class) end let :attribute do :text end let :mock_class do double('model').tap do |mock| allow(mock).to receive(:class) { ActiveRecord::Base } allow(mock).to receive(:errors) {errors} allow(mock).to receive(:read_attribute_for_validation) {attribute_value} end end subject do validator.validate_each(mock_class, attribute, attribute_value) errors.empty? end context 'ただしくHogeってる時' do let :attribute_value do 'hoge' end it 'バリデーションが通ること' do expect(subject).to be_truthy end end context 'Hogeってないとき' do let :attribute_value do 'fuga' end it 'バリデーションが通らない' do expect(subject).to be_falsey end end end
今回は省略しましたが、エラーメッセージに関するテストを行うとさらに良いと思います。
クラウドIDEを使ってみた Nitrous.io & Codeanywhere
はじめに
お久しぶりです。昨年末から関ってきた某プロジェクトを5月にリリースし、最近やっと落ち着いてきました。落ち着いてきたついでに先週末ごろから開発環境を0から作り直しているのですが、開発環境の構築って面倒くさいですよね。検証環境なんかはVagrantを使えば良いかもしれません、しかし、開発環境となると話は別です。IDEをインストールしてエディタの設定をして...etc 面倒くささの極み!!!
比較
双方一番安い有料プランに登録しての比較です。
仮想環境について
Codeanywhere
- Redhat系
- sudo権限あり
- ブラウザ上のターミナル以外から接続する方法なし
IDEについて
Codeanywhere
- 自動でオートコンプリートリストが開く
パフォーマンスについて
Codeanywhere
- 非常に不安定
料金について
Codeanywhere
- $2から
- マシンの設置可能台数がプランによって変化
まとめ
「リソースいっぱい使って、いくつも環境作りたい!」という人は料金体系的にCodeanywhereをお勧めしたいところですが、前述したとおり動作が非常に不安定です。日本国内から使用するなら安定性とパフォーマンスの問題から、現時点では Nitrous.io しか選択肢はなさそうです。
nginxモジュールのテストを書く
はじめに
唐突ですが、仕事でシステムの構成を考えていると、nginxを拡張したいなーとか思うことってあると思います。nginxはC言語で割と簡単に拡張モジュールを書くことができます。(今回は拡張モジュールの書き方については触れませんが・・・)
だがしかし、仕事なんだからテストコードは必須だよね(迫真)。既存のnginx用のモジュールのコードを眺めてみると、謎の`t`フォルダにテストコードが入っています。
`t`ってなんだよ`t`って!中身もhogehoge.tとかなってるし、t言語なんかあるの? というところから始まり、なんとかテストを書いて実行できるようになったので方法をまとめておきます。
具体的にはperlのテストフレームワークと agentzh/test-nginx · GitHub を利用してテストを書いていきます。
テストの準備
必要なもの
nginxのテストをするために必要なソフトウェアやライブラリです。URLを掲載していないものはUbuntu13.10の公式リポジトリからSynapticを使ってインストールしました。
nginxのコンパイルに必要なもの
- nginx (http://nginx.org/download/)
- echo-nginx-module (https://github.com/agentzh/echo-nginx-module)
- memc-nginx-module (https://github.com/agentzh/memc-nginx-module)
テストを実行する上でecho-nginx-moduleとmemc-nginx-moduleは不可欠でした。
※`/home/develop` 下にecho-nginx-moduleとmemc-nginx-moduleをcloneしていると想定して記事を書いています。
perlでテストを実行するために必要なもの
- perl
- prove
- libtest-base-perl
- libtest-longstring-perl
- test-nginx (https://github.com/agentzh/test-nginx)
test-nginxは適当なディレクトリにcloneしておき、テスト実行時にディレクトリを指定します。
※`/home/develop' 下にtest-nginxをcloneしていると想定して記事を書いています。
テストコードの書き方
nginxモジュールのプロジェクトディレクトリに`t/`というディレクトリを作り、その中に`{任意の名前}.t`という形でテストコードを書いていきます。
use Test::Nginx::Socket; # テストの実行回数の設定。 repeat_each(3); plan tests => repeat_each() * 2 * blocks(); # 文字の表示をわかりやすくする。 no_long_string(); # テストの実行 run_tests(); __DATA__ === TEST 1: ハローワールドのテスト --- config location /hello { echo hello_world; } --- request GET /hello HTTP/1.0 --- response_body hello_world --- error_code: 200
`--- config` にnginxの設定を記述し、`---request` でテストとして実行するリクエストを記述します。
次に、リクエストに対して`--- response_body` で期待するレスポンスを記述し、 `--- error_code` に期待するレスポンスコードを記述します。
こんなに簡単にかけるテストは見たことはありません!幸せ。
テストの実行
大まかな流れ
nginxのテストの大まかな流れは次のとおりです。
nginxのコンパイル
テストを実行するために、自分のモジュールを組み込んだnginxをコンパイルします。私の場合は次のようなオプションでコンパイルしました。
CFLAGS="-O1 -Wall -g" ./configure --add-module=[自分のモジュール] --add-module=/home/develop/echo-nginx-module --add-module=/home/develop/memc-nginx-module --prefix=/home/develop/nginx --with-debug
特筆すべき点は下記2点です。
- CFLAGSはvalgrindを利用してプロファイリングを行うことを想定しています。
- プレフィックスをつけて通常使用するnginxとは分けました。(当然といえば当然ですね。)
proveでテストの実行
まず、先ほどコンパイルしたnginxにパスを通します。
export PATH=/home/develop/nginx/sbin:$PATH
そしてテストを実行します。
prove -I/home/develop/test-nginx/lib -r t
proveを実行する際にtest-nginxをincludeのがポイントです。
valgrindでプロファイリングする場合
メモリリークや実行速度についての解析をvalgrindを用いて行う場合にはTEST_NGINX_USE_VALGRINDという環境変数にオプションを記述します。
TEST_NGINX_USE_VALGRIND="{オプション}" prove -I/home/develop/test-nginx/lib -r t
メモリリークのテストをする場合にはこんなかんじでしょうか。
TEST_NGINX_USE_VALGRIND="-v --tool=memcheck --leak-check=full" prove -I/home/develop/test-nginx/lib -r t
RailsのEnumと相性が悪い人向けのInumをバージョンアップさせた話。
はじめに
Railsの列挙型事情
Rails4.1以前では公式に列挙型が実装されておらず、列挙型を実現するために様々なgemが生み出されてきましたが、Rails4.1でついにActiveRecord::Enumという列挙型のクラスがやっと実装され、長い列挙型宗教戦争が終わろうとしています。
どうせI18n対応とかじゃないの?
もちろんI18n対応も理由の一つですが、I18n対応だけで良ければ brainspec/enumerize · GitHub あたりを使うのがベストだと思います。それだけでは足りないほしがりさん向けです。
Inumができること
モデルにロジックを食わせてコレ以上肥満体型にしたくない!
そんなあなたにInumダイエット。Inumでは列挙型を独立したクラスとして定義するので、モデルに列挙型に関するロジックを書いて太らせるなんてことはありません。(メンテナンス継続理由の大半を占めています。)
class AnimeType < Inum::Base define :NYARUKOSAN, 0 define :NONNONBIYORI, 1 define :KILLMEBABY, 2 # セリフを表示する def serif case self when NYARUKOSAN "いつもニコニコあなたの隣に這いよる混沌、ニャルラトホテプ、です!" when NONNONBIYORI "にゃんぱすー" when KILLMEBABY "キルミーベイベーは復活する、絶対にだ!!" end end end p AnimeType::KILLMEBABY.serif # => "キルミーベイベーは復活する、絶対にだ!!"
各位@ActiveRecordとなかよく
バージョン3.0.0には ActiveRecord support. by alfa-jpn · Pull Request #4 · alfa-jpn/inum · GitHub super friendly ActiveRecord. という内容のプルリクがマージされています。超仲いいです。
class Anime < ActiveRecord::Base bind_inum :column=> :anime_type, :class => AnimeType end
この1行書くだけで、Animeモデルのanime_typeカラム(integer)に列挙型がバインドされ便利なメソッドが定義されます。
anime = Anime.new # getter/setterで列挙型にラップされます。 anime.anime_type = AnimeType::KILLMEBABY p anime.anime_type.value # => 2 # 判定用メソッドが追加されます。プレフィックスにカラム名が入りますが、bind_inumのオプションで消せます。 p anime.anime_type_nonnonbiyori? # => false
仲がいいものの依存はしておらず、ActiveRecordは必須ではありません。
I18nで表示名を付けたい
I18nのyamlでinum下に記述することでtranslateできます。
ja: inum: anime_type: nyarukosan: '這いよれ!ニャル子さん' nonnonbiyori: 'のんのんびより' killmebaby: 'キルミーベイベー'
translate メソッドもしくは t メソッドで翻訳できます。
p AnimeType::NONNONBIYORI.t # => "のんのんびより"
パワフルにパーズできる
わりと何でもパーズしてくれるメソッドが内部にいるので、代入や比較ができます。
# パーサー p AnimeType.parse(0).t # => "這いよれ!ニャル子さん" p AnimeType.parse('NONNONBIYORI').t # => "のんのんびより" p AnimeType.parse(:KILLMEBABY).t # => "キルミーベイベー" p AnimeType.parse(999) # => nil # ActiveRecordのsetterでもパーサーが働くのでこんなことや anime = Anime.new anime.anime_type = "KILLMEBABY" # わりと色々な箇所に仕組んでいるのでこんな風にも使えます。 p (AnimeType::NONNONBIYORI + 1 ).t # => "キルミーベイベー"
EnamerableとComparebleをinclude
わりと無双な感じ
p AnimeType::KILLMEBABY == 2 # => true p AnimeType::NONNONBIYORI < AnimeType::KILLMEBABY # => true AnimeType.each do |enum| p enum.t end
参考サイト
Rails4と3で論理削除を行うためのGem Kakurenbo の紹介と今更論理削除Gemを実装した理由。
様々なわけがあってkakurenboは非推奨です。新規のプロジェクトではkakurenbo-putiの使用をおすすめします。
Kakurenboとは
Rails4及びRails3で論理削除を行うためのGemです。paranoia及びacts_as_paranoidと互換性があるので、gemを置き換えるだけでそのまま動きます。(動かなかったらissue投げてくだしあ><)
なんで今更論理削除のGemを実装したのか
今更開発しなくても論理削除のGemは山ほど出てきますが、なぜ開発しなくてはならなくなってしまったのか経緯に触れておきます。
acts_as_paranoid
論理削除の本家と言ってもいいレベルの知名度です、だがしかしいつまで立ってもRails4に対応しない。
rails4_acts_as_paranoid
既存のバグほったらかしで使えない。(メモリ関連のバグがありSQLクエリが永遠に結合されていきます。やばみ。プルリク送ったのに放置されてます。)
paranoia
今Rails4で論理削除と言ったらコレ!ってくらい定番です。私もつい最近まではコレを利用していました。コールバック関連のバグを修正するプルリク送ったりして結構便利に使っていました。
しかし、とある作業中にアソシエーションに深刻な問題があることに気付き、プルリクを送りましたが、仕様なのでマージする気はないとプルリク蹴られてしまいました。
具体的にはリレーショナルで1対多の関係を設定している場合、親を論理削除して復元を行った場合、親が論理削除される以前に論理削除されている子まで復元されてしまうのです。例えば掲示板で、掲示板を本体を論理削除した後、何かの手違いだとわかり復元する時に、問題があって削除してた書き込みも全て復元されてしまうのです。
「そもそも間違えて削除しなければいい話だろ」なんて運用でカバーするスタイルはRails、Rubyらしくありません。だからこれは深刻な問題です。大切なことなのでもう一度言います。これは深刻な問題です。
インストール方法
いつものようにGemfileに追加するとか、インストールするとか。
gem "kakurenbo"
使い方
論理削除で管理したいモデルに deleted_at カラムを datetime 型で追加するだけで使えます。
rails generate migration add_deleted_at_to_models deleted_at:datetime
クラスの初期化時にカラムを読み取って、deleted_atがあれば論理削除と判別するので論理削除のモデルでいちいちacts_as_paranoidメソッドを呼び出す必要はありません。
(deleted_at以外のカラムを利用する際には、acts_as_paranoidをcolumnオプション付きで呼び出して設定してください。)
基本的な使い方
paranoiaやacts_as_paranoidと互換性があるので説明はいらないかもしれませんが基本的な使い方です。
この記事は古くなっています、最新バージョン 0.2.x の使い方はこちらを参照してください。
論理削除Gem、 Kakurenbo をアップデートしました。 - 波打際のブログさん
復元
論理削除したモデルはrestoreで復活できます。
model.restore! # 次の方法も使えます。id指定、もしくはidの配列で一括で復元できます。 Model.restore(id)
before_restore, after_restoreコールバック使えます。
物理削除
destroy!で物理削除できます。
model.destroy!
スコープ
# 削除されたモデルも取得 Model.with_deleted # 削除されたモデルのみ取得 Model.only_deleted # 削除されていないモデルのみ取得(デフォルトスコープです。) Model.without_deleted
関連リンク
修正履歴
- 2014年01月29日 誤字脱字を修正しました。
Ubuntu13.10でiBusを捨ててfcitxを使う
はじめに
Ubuntu13.10がリリースされて3ヶ月ほど立ちました。Ubuntuのサポート期間はLTS版を除き、9ヶ月というとても短い設定となっています。今まで使用してたUbuntu13.04のサポート期限は去年の12月いっぱいなので、継続して使用するためにUbuntuのバージョンを13.10にアップグレードしたところ、インプットメソッドのiBusが1.5にアップグレードされ、非常に使いにくいものとなっていました。
はじめのうちはiBusと仲良くなる努力はしましたが不可能であることを悟り、今話題のfcitxを利用することにしました。これは導入に関するまとめです。
fcitxの良い所
- iBus1.4と同様の感覚で日本語入力ができる。
- iBusよりもカスタマイズできる箇所が多い。(候補ウィンドウの配色やフォント、テーマなど)
- Mozcのユーティリティへのアクセスがしやすい。(設定ツールや辞書ツールをワンクリックで呼び出せる)
fcitxの悪い所
- 半角/全角キーを長めに押さないとIMが切り替わらない事がある。(もしかしたら設定の問題?)
導入
パッケージのインストール
Ubuntuソフトウェアセンターか、apt-getでfcitx-mozcをインストールすると、依存関係が設定されているので必要なものがすべて芋づる式でインストールされます。
デフォルトでfcitxを使用する設定にする
パッケージのインストールが完了したら システム設定の言語サポートから、「キーボード入力に使うIMシステム」の設定をfcitxに設定します。設定が完了したらコンピュータを再起動させます。
iBusのインジケータを非表示にする
再起動するとfcitxのインジケータと、iBusのインジケータが表示されていると思うので、システム設定のテキスト入力から、「メニューバーに現在の入力ソースを表示」のチェックボックスを外し、iBusのインジケータ表示を無効にします。
fcitxの設定
この設定が完了すると、fcitxは既に使用できる状態になってますが、デフォルトでは半角/全角に割り当てられているキーマップが、mozcの半角全角と競合してしまっているために、おかしな挙動になってしまいます。そこで、mozcに割り当てられている半角/全角キーのキーマップを全て削除します。
メニューバーのfcitxのアイコンをクリックし、Mozcツール(表示されていない場合はテキスト入力画面で半角/全角キーを押して、IMをMozcに切り替えてください)から設定ツールをクリックします。Mozc設定ツールが起動したら、一般タブの中のキー設定項目右側にある編集ボタンをクリックするとキー設定ウィンドウが表示されます。
キー設定ウィンドウの上部にある入力キーのタブをクリックし、入力キーの名前順にリストを並び替え、Hankaku/Zenkakuの設定4つ全てを削除します。
以上で設定は完了です、設定が完了したら念の為に再起動を行います。
Ruby on RailsのI18nで使用する名前空間に関してのまとめと、ベストプラクティスの検討。
はじめに
Ruby on RailsにはI18nというgemが標準で同梱されており、特に何もせずとも文章の国際化を行うことができる仕組みになっています。ちなみに、I18nとはinternationalizationを表しており、iとnの間に18文字あるから `I18n` だそうです。
最近関わっているRailsのプロジェクトで国際化を行うタスクが発生し、I18nの辞書ファイルを定義する際に、定義ファイル中の名前空間(ロケール情報を定義する場所)について、どのような構造で配置するべきか調べる機会があったのでまとめておきます。
環境
- Rails4
想定環境
rails generateで name, email, urlを引数に指定してUserをscaffoldした環境を想定してロケールファイルを記述しています。
rails generate scaffold user name:string email:string url:string
Userモデル
User | name | url |
---|
※idとかcreated_atとかupdated_atは気にしないでください。
app/views/users フォルダ
app/views/users |
---|
_form.html.erb |
index.html.erb |
show.html.erb |
edit.html.erb |
※他にも生成されますが、これくらいで許してください。
Rails公式ガイドのまとめ。
Railsの公式ガイドを Rails Internationalization (I18n) API — Ruby on Rails Guides を読み解いた結果、ロケールファイルは次のような名前空間の構造で記述すれば良いことがわかりました。
ja: # モデルは全て activerecord 以下に記述する。 # これにより、User.model_name.human / User.human_attribute_name({attr_name})でアクセス可能。 activerecord: models: user: 'ユーザ情報' attributes: user: name: '名前' mail: 'メールアドレス' url: 'ウェブページ' errors: models: user: blank: 'urlかmailが空白です。' attributes: name: blank: '名前が空白です。' # errosの中に同じ定義がある場合、ネストが深いほうが優先順位が高い # ビューはビューを格納しているフォルダ名を起点にし、ビュー名毎に記述する。 # 対応するビューの中ではツリーを省略できる。<%= t '.title' %> users: index: title: 'ユーザ一覧' show: title: '%{user_name}さんのユーザ情報' edit: title: '%{user_name}さんのユーザ情報を編集' # グローバルな感じのエラーはerrorsを起点にする。 errors: nanikano_error: 'よくわからないけど例外です。'
上記の形式で記述することで、Railsのヘルパーメソッドを利用して、容易にロケール情報へアクセスすることができます。
Modelのロケール情報へのアクセス
モデル関連のロケール情報を、上記の形式で書くことで ActiveRecord::Base に定義されている、model_name.humanとhuman_attribute_name を利用して取得することができます。
p User.model_name.human # => `ユーザ情報` (activerecord.models.userを参照) p User.human_attribute_name(:name) # => `名前` (activerecord.attributes.user.nameを参照)
Viewからロケール情報へのアクセス
ビュー関連のロケール情報を、上記の形式で書くことで、対応するビューの中で t ヘルパーメソッドを利用して、キー名を省略した形で取得することができます。
# index.html.erbの中で実行 <% t '.title' %> # => 'ユーザ一覧' (users.index.titleを参照) # show.html.erbの中で実行 <% t '.title', user_name: user.name %> # => '{ユーザ名}さんのユーザ情報' (users.show.titleを参照)
HTMLをロケールの中に記述する
通常、ロケールファイルの中にHTMLタグを記述しても、タグがエスケープされて出力されます。しかし、キーの末尾を _html にすることで、ロケールファイル内に書かれたHTMLタグをそのまま出力することが可能です。
ja: users: index: hello_html: 'Hello!<strong>Ruby</strong>World!'
ただし、ロケールにHTMLタグを持たせることには抵抗があります。言語ファイルがビューのスタイルに影響を及ぼしてしまうことはロケール情報の範疇を超えていると個人的には考えています。なので、他に綺麗に記述することができない、止む終えない場合の最終手段程度に留めておくことをお勧めします。
オレオレベストプラクティス
Rails公式ガイドでは ヘルパー や コントローラ、共通のワード等の名前空間の構造には触れられていませんでした。そこで、stackoverflowや既存のロケールファイルを参考に、オレオレベストプラクティス的な物を考えてみました。
ja: # 共通の使いまわす可能性のあるワードは dictionary を起点にする。 dictionary: messages: hello_user: 'こんにちは%{user_name}さん' words: user: &user 'ユーザ情報' # &user はエイリアスで *user で参照できる。(同一ファイル内のみ有効) user_copy: *user # => 'ユーザ情報' となる。 site_name: '波打際のブログさん' # モデルは全て activerecord を起点にする。 # これにより、User.model_name.human / User.human_attribute_name({attr_name})でアクセス可能。 activerecord: models: user: 'ユーザー情報' attributes: user: name: '名前' mail: 'メールアドレス' url: 'ウェブページ' errors: nanikano_model_error: 'モデルで何かのエラー' # 特定のモデルに属さないエラーはこの階層に書く。 models: user: blank: 'urlかmailが空白です。' # モデル内で使いまわすエラーはこの階層に書く。 attributes: name: blank: '名前が空白です。' # errosの中に同じ定義がある場合、ネストが深いほうが優先順位が高い # 全モデルで共通のアトリビュートはattributesを起点にattribute名を直下に記述する。 # validation errorやhuman_attribute_nameで取得できる。 # (ネストの深いものが優先されるため、activerecord.attributes内の優先度が最も高い。) attributes: user_id: 'ユーザー' # ヘルパー関数はhelpersを起点にする。 # ヘルパー関数内で tメソッドを使用すると、呼び出し元のビューに基づいたパスが呼び出される。 # 例えばusersのshowから呼ばれたヘルパー関数内で t('.hoge') を実行した場合 users.show.hoge が参照される。 # 呼び出し元によって文言を変えたい場合はビュー側に記述する。(そんなことあるかわからないけど。) helpers: user: welcome: 'ようこそ!' # ビューはビューを格納しているフォルダ名を起点にし、ビュー名毎に記述する。 # 対応するビューの中ではツリーを省略できる。<%= t '.title' %> users: index: title: 'ユーザ一覧' show: title: '%{user_name}さんのユーザ情報' edit: title: '%{user_name}さんのユーザ情報を編集' # ActionMailerを継承したMailerはクラス名を起点にする。 nanikano_mailer: alert_email: subject: 'タイトル' body: '本文' # グローバルな感じのエラーはerrorsを起点にする。 errors: nanikano_error: 'よくわからないけど例外です。'
コントローラに関する記述が無いと思いますが、コントローラで固有の言語情報を持つ設計は、そもそも設計が誤りである可能性があります。もしコントローラにロケールしなければならない文字列がある場合には、設計を見直してみてください。
(redirect_toメソッド呼び出しの際の、noticeやalertメッセージに関しては、文量がさほど多くなく割と使い回しているため、dictionary.messages以下に突っ込んでしまっています。100行200行にでもなったら考えましょ。)
あくまでも オレオレベストプラクティス なので、おかしいところ、もっとこうする方がいいんじゃね?とかあると思います。もしそう思ったら指摘してくれると嬉しいです。