woshidan's loose leaf

ぼんやり勉強しています

Enumerizeでenum値を扱う

Railsのモデルでenum値を扱うとき、enumerizeというgemを使うと便利です。

# Gemfile

gem 'enumerize'
class Task
  extend Enumerize
  enumerize :status, in: { pending: 0, todo: 1, done: 2 }
end

と書くと、

Task = Task.new(status: 'pending') #=> valid
Task = Task.new(status: 'unknown') #=> not valid

のようにバリデーションがかけられたり、

Task.where(status: :pending)

のように検索を行うことが可能です。

また、以下のようにlocaleを設定することで

# ja.yml
enumerize:
  task:
    status:
      pending: 待機中
      todo: 対応する
      done: 対応済
Task.status.options # => [["待機中", "pending"], ["対応する", "todo"], ["対応済", "done"]]

のように値を返すようになり、enum値の入力でよく使うselectフォームで

<%= form_for @task do |f| %>
  <%= f.select :task, Task.status.options %>
<% end %>

のように簡潔に記述できるようになります。

参考

doruby.jp

github.com

定数値を環境ごとに分岐したいだけならRails.env.xxx? やconfig/environments以下のファイルに値を書くより環境変数にくくり出した方がよさそう

Railsで開発していると

if Rails.env.production?
  # production環境用の処理
elsif Rails.env.staging?
  # staging環境用の処理
else
  # それ以外の環境用の処理
end

という風に環境ごとの処理を書いたり、config/environments以下のproduction.rbやstaging.rb, development.rbに config.xxx と各環境用の値を設定して、Rails. configuration.xxxと呼び出すことができます。

しかし、環境ごとに変わる部分が単なる定数値であれば環境変数にして

ENV['SOME_VALUE']

のように参照することでproduction.rbやstaging.rbに値をハードコーディングせずに済ませることができます(代わりにdotenvなどを使い、.envファイルを共有したりします)。

こうしておくと環境ごとの値を変更したい場合リポジトリにコードをコミットせず済ませることができるので、処理の流れは変更しないが利用する外部リソースのURIなどを試行錯誤するときなどでは楽になります。

参考

railsdoc.com

qiita.com

pikawaka.com

画面の横幅が大きい(小さい)ときのみ要素を表示する

画面を作成していると単純に要素を横に伸ばしたりでスマホとPCの表示が作れず、要素の順番や配置が変わってしまうことがあります。

その度合いがまるきり別物ならPC版とスマホ版で別のテンプレートを使えばよさそうですが、順番が数カ所違う程度ならスマホ版だけ、PC版だけ要素を表示する、という方法が取れればよさそうです。

BootStrapを使っている場合、visible-xx/hidden-xxを使うとxxのところに入った大きさに画面がなったら要素の表示/非表示を切り替えてくれます。

スマホの場合、たとえば画面サイズがxs(767px)以下なら表示するなら、visible-xsをクラスに追加し、画面サイズが768px以上なら表示しないならhidden-xsをクラスに追加することで実現できます。

<div class="hidden-xs" style="background-color: red;"></div><!-- 画面幅が768px以上のときだけ赤い要素が表示される -->
<div class="visible-xs" style="background-color: blue;"></div><!-- 画面幅が767px以下になると青い要素が表示される -->

参考

qiita.com

studio-key.com

少しだけ動作が速くなるコードの書き方3つ

レコードがあることを条件にしたいとき、findの代わりにexists?を使う

if User.find_by(email: "...")
  # something.todo
end

のとき、findやfind_byではActiveRecordインスタンスが生成されてしまう分遅いので、存在確認をしたいだけなら下記のようにexistsを使うとよいようです。

if User.exists?(email: "...")
  # something.todo
end

speakerdeck.com

多数のレコードを処理するとき、eachではなく、find_in_batchesを使って一度に生成するインスタンス数を制限する

User.all.each do |user|
  # something.todo
end

としたいとき、

User.find_in_batches(of: 100) do |users|
  users.each do |user|
    # something.todo
  end
end

qiita.com

正規表現にマッチするかどうか調べるとき、=~よりmatch?の方が速い

str =~ /regexp/

より

str.match?(/regexp/)

speakerdeck.com

Railsのuniquenessバリデーションについて

uniquenessバリデーションを使うと、その項目の中で一意であるようにバリデーションがかけられます。

class User
  validates :email, uniqueness: true
end

とすると、emailはUserの中で一意となるようにRails側でバリデーションをかける。

これを、Userが会社に属していて会社の中で一意になるようにバリデーションをかけようとすると

class User
  validates :email, uniqueness: { scope: :company_id }
end

のように、scopeを使って、一意になる範囲を指定する。これをさらに、(例がいいかは少しおいておいて)会社の中で複数の部署に属するユーザはその都度User登録しようとかいう話になって、ある会社のある部署の中で一意というのが書きたいときは、

class User
  validates :email, uniqueness: { scope: [:company_id, :group_id] }

のように、scopeに配列を渡す形となります。

参考

railsguides.jp

310nae.com

RSpecでテストデータ用にファイルを読み込む

RSpecでテスト用のファイルを読み込む

RSpecでテスト用のファイルを読み込む際はデフォルトでは spec/fixtures/files にファイルを置いて

file_fixture("example.txt").read

とすると、ファイルの中身がspec中で呼び出せる。

ファイル中にHTMLやJSONのレスポンスを入れておいてHTTPリクエストをモックしたい場合などに使える。

既にfixturesに他にファイルがあってそれらに合わせてファイルの場所を既定のパスから変更したい場合は

RSpec.configure do |config|
  config.file_fixture_path = "spec/custom_directory"
end

で可能となる。

RSpecでファイルをアップロードする

テストデータでファイルを使いたい状況としては、HTTPリクエストのモック以外にもファイルのアップロードのテストなどがある。 その場合は fixture_file_upload を以下のように使う。

post :change_avatar, avatar: fixture_file_upload('files/spongebob.png', 'image/png')

このメソッドで使うファイルのパスは config.file_fixture_path ではなく、config.fixture_path で指定するので注意。

参考

relishapp.com apidock.com qiita.com

配列の要素全てが該当する条件を記述するallマッチャ

scopeみたいにフィルタリングした要素について、フィルタリング対象の要素について

let(:included) { Item.new(price: 250) }
let(:not_included) { Item.new(price: 300) }
let(:array) { [included, not_included] }

subject(array.filtering)

it '250円以下の品物が絞り込まれる' do
  expect(subject).to contain_exactly(included)
  expect(subject).not_to contain_exactly(not_included)
end

のように、フィルターの境界値の値を持つ要素をフィルタリング前のコレクションに含めておいて、その要素が含まれる/含まれない、といった形でspecを書いていくこともできますが、allマッチャを使うことで

let(:sample1) { Item.new(price: 250) }
let(:sample2) { Item.new(price: 300) }
let(:array) { [sample1, sample2] }

subject(array.filtering)

it '250円以下の品物が絞り込まれる' do
  expect(subject.map(&:price)).to all(be <= 250)
  expect(subject.count).to eq 1
end

元の要素ではなく、絞り込まれた結果に注目してspecを書くことができます。

参考

relishapp.com