32. Campus モデルと enum

複数形の登録

キャンパスのモデルである。これも同様であるが、一つだけハマリポイントがあった。generator で campus モデルを作成しようとしたら、campu の複数形と判断されてしまった。旧システムの情報を参考に config/initializers/inflections.rb に campus の複数形である campuses を登録した。あとで作成する syllabus の複数形である syllabuses も同時に登録しておいた。システムで数回しかやらない設定はどうしても忘れてしまう。

ActiveSupport::Inflector.inflections do |inflect|
  inflect.irregular("campus", "campuses")
  inflect.irregular("syllabus", "syllabuses")
end

この設定をして generator も動作した。

マイグレーションと FactoryGirl の設定

モデルの中身は以下のようになる。

spec/support/create_factories/campus_factory.rb は以下のようにした。引数は sort_order の enum キーになっており、Campus.sort_orders hash を引くことで実際の値(1〜3)になる。

CampusFactoryHash = {
  shinagawa: %w( 品川 S ), arakawa: %w( 荒川 A ), both: %w( 両 B )
}

# @param [Symbol, String] key オブジェクトを一意に決定するキー
# @return [Campus] Campus FactoryGirl オブジェクト
def campus_factory(key)
  keysym = key.to_sym
  n, c = CampusFactoryHash[keysym]
  FG.find_or_create(
    :campus, name: n, code: c, sort_order: Campus.sort_orders[keysym]) if n
end

# @return [Array<Campus>] Campus FactoryGirl オブジェクトの配列
def campus_all_factories
  CampusFactoryHash.keys.map { |k| campus_factory(k) }
end

モデルのテスト

model spec は以下の通り。Gakunen と同様、nil 不可の確認、削除不可の確認、一意性の確認をする。

require 'rails_helper'

RSpec.describe Campus, :type => :model do
  context "common validation check" do
    before do
      @target = campus_factory :shinagawa
    end

    check_presence_validates %i( name code sort_order )
    check_reject_destroy_validates
    check_unique_validates(%i( code sort_order )) { campus_factory :arakawa }
  end

次にクラスメソッド。subject は Campus。campuses は品川と荒川だけを取得する。with_both をつけた時だけ両キャンパスも得る。

  context 'after all campuses are registrered' do
    before do
      @campuses = campus_all_factories
    end

    context 'for Campus class' do
      subject { Campus }

      it 'should receive campuses, campuses_with_both' do
        target_method_send_test subject,
          :campuses, nil, @campuses[0..1],
          :campuses_with_both, nil, @campuses
      end

キャンパスが含まれるキャンパス一覧を得る campus_included(c) をテストする。品川の場合には、品川と両、荒川の場合には荒川と両が得られることを期待する。default(ip) は ip アドレスからキャンパスを得るメソッドである。テストの時には品川になることを期待する。

      it 'should receive campuses_included, default' do
        s, a, b = @campuses
        target_method_send_test subject,
          :campuses_included, s, [ s, b ],
          :campuses_included, a, [ a, b ],
          :default, '荒川のIPその1', a,
          :default, '荒川のIPその1'', a,
          :default, '127.0.0.1', s,
          :default, '品川のIPその1'', s
      end

クラスメソッド最後は、get_arakawa、get_shinagawa、get_both でそれぞれのキャンパスが得られることを期待する。以前は、arakawa, shinagawa, both というメソッドであったが、enum によりこれらのメソッドが定義されているので、名前を変えた。

      it 'should receive get_arakawa, get_shinagawa, get_both' do
        s, a, b = @campuses
        target_method_send_test subject,
          :get_shinagawa, nil, s,
          :get_arakawa, nil, a,
          :get_both, nil, b
      end
    end

次はインスタンスメソッド。subject は @campuses。まず、shinagawa?、arakawa?、both? でキャンパスを診断できることを期待する。

    context 'for Campus instances' do
      subject { @campuses }

      it 'should receive arakawa? shinagawa? both?' do
        target_array_method_send_test subject,
          :shinagawa?, nil, [ true, false, false ],
          :arakawa?, nil, [ false, true, false ],
          :both?, nil, [ false, false, true ]
      end

include_XXX? で各キャンパスを含むかどうかを調べられることを期待する。

      it 'should receive include_arakawa? include_shinagawa?' do
        target_array_method_send_test subject,
          :include_shinagawa?, nil, [ true, false, true ],
          :include_arakawa?, nil, [ false, true, true ]
      end

ename で英語名、name_with_campus で XX キャンパスという文字列が得られることを期待する。

      it 'should recieve ename, name_with_campus' do
        target_array_method_send_test subject,
          :ename, nil, %w( shinagawa arakawa both ),
          :name_with_campus, nil,
            %w( 品川キャンパス 荒川キャンパス 両キャンパス )
      end

include_camplus?(campus)で campus が含まれることを期待する。これで取り急ぎのテストは終了。

      it 'should receive is_campus?' do
        s, a, b = @campuses
        target_array_method_send_test subject,
          :include_campus?, s, [ true, false, true ],
          :include_campus?, a, [ false, true, true ],
          :include_campus?, b, [ false, false, true ]
      end
    end
  end
end

モデルの実装

上記のテストが通過するモデルを実装する。基本的には今まで通りなので、enum に関わる注意すべきところだけ記述する。

enum は sort_order に設定する。この記述により、shinagawa?、arakawa?、both? は実装することなくテストが通過する。

  enum sort_order: { shinagawa: 1, arakawa: 2, both: 3 }

enum に設定した key に対応する値は Campus.sort_orders[key] で取得する。これを使って scope を設定する。

  # @return [Array<Campus>] sort_order 順に並んだキャンパス一覧を得る
  scope :order_sort_order, -> { order arel_table[:sort_order] }
  # @return [Array<Campus>] 両キャンパスを除いたキャンパス一覧を得る
  scope :without_both, -> { where arel_table[:sort_order].not_eq(Campus.sort_orders[:both]) }
  # @return [Array<Campus>] 両キャンパスを除いた整列済キャンパス一覧を得る
  scope :campuses, -> { without_both.order_sort_order }
  # @return [Array<Campus>] 両キャンパスを含んだ整列済キャンパス一覧を得る
  scope :campuses_with_both, -> { order_sort_order }
  # @return [Array<Campus>] キャンパスを含むキャンパス一覧を得る(必ず両が含まれる)
  scope :campuses_included, -> c { where arel_table[:sort_order].in([ Campus.sort_orders[c.sort_order], Campus.sort_orders[:both]]) }

Campus.shinagawa とすると、sort_order の値が :shinagawa であるものの配列が得られる。今回は一つの値しかないので、take で一つ取得する。他も同様である。

  # @return [Campus] 品川キャンパス
  def self.get_shinagawa
    shinagawa.take
  end

  # @return [Campus] 荒川キャンパス
  def self.get_arakawa
    arakawa.take
  end

  # @return [Campus] 両キャンパス
  def self.get_both
    both.take
  end

include_shinagawa? は品川か両であれば true になる。enum により shinagawa? || both? と書くだけで済んでしまう。include_arakawa?、include_campus?(campus) も同様である。

  # @return [Boolean] 品川キャンパスであれば true (両キャンパスも true)
  def include_shinagawa?
    shinagawa? || both?
  end

  # @return [Boolean] 荒川キャンパスであれば true (両キャンパスも true)
  def include_arakawa?
    arakawa? || both?
  end

  # @return [Boolean] キャンパスが c を含むか?
  def include_campus?(c)
    sort_order == c.sort_order || both?
  end
end

sort_order はデータベース内では数値だが、ActiveRecord 内では Symbol になる。そのため、ename は sort_order を返せばよい。

  # @return [String] 英語名
  def ename
    sort_order
  end

今日はここまで。

written by iHatenaSync