ActiveRecord / Rails / Ruby

この記事ではBlog::Post#tagsの数を5つまでに制限する場合のサンプルコードを紹介します。

モデルでのValidation設定

まずモデル側でpost_tagsLengthValidationを設定します。

# app/models/blog/post.rb
class Blog::Post < ActiveRecord::Base
  MAX_POST_TAGS_LENGTH = 5

  has_many :post_tags
  has_many :tags, through: :post_tags

  validates :post_tags, length: {maximum: MAX_POST_TAGS_LENGTH}
end

ロケール辞書の設定

Validation対象のフィールド名やエラーメッセージを設定します。

# config/locales/models/blog/post/ja.yml
ja:
  activerecord:
    errors:
      models:
        blog/post:
          attributes:
            post_tags:
              too_long: 'は%{count}個までしか登録できません'
    models:
      blog/post: ブログ記事
    attributes:
      blog/post:
        id: 管理ID
        post_tags: タグ

テストケースの追加

正しくValidationできることをテストします。

#spec/models/blog/post_spec.rb
describe Blog::Post, type: :model do
  context 'with validations' do
    it do
      should validate_objects_length_of(:post_tags).
               is_at_most(Blog::Post::MAX_POST_TAGS_LENGTH).
               with_message("タグは#{Blog::Post::MAX_POST_TAGS_LENGTH}個までしか登録できません")
    end
  end
end

validate_objects_length_ofカスタムマッチャの追加

テストでさらっとvalidate_objects_length_ofマッチャを使いましたが、これはカスタムマッチャなので下記のコードをspec/support/matchers.rbに追加します。

# spec/support/matchers.rb
RSpec::Matchers.define :validate_objects_length_of do |field|
  def is_at_most(size)
    @max_size = size
    self
  end

  def with_message(message)
    @message = message
    self
  end

  match do |model|
    model_factory_name = model.class.table_name.singularize
    association_name = field

    association =
      model.
        class.
        reflect_on_all_associations(:has_many).
        find { |i_association| i_association.name == association_name }

    if association.nil?
      @failure_appendix = "(#{association_name.inspect} has-many association does not exist)"
      return false
    end

    factory_args = [association.table_name.singularize]

    record = create(model_factory_name)

    if @max_size
      ((record.send(association_name).size)..@max_size).each do |size|
        unless record.valid?
          @failure_appendix = "(invalid with #{size} #{'object'.pluralize(size)})"
          return false
        end
        record.send(association_name) << build(*factory_args)
      end

      if record.valid?
        size = @max_size + 1
        @failure_appendix = "(valid with #{size} #{'object'.pluralize(size)})"
        return false
      end
    end

    if @message
      if record.errors.full_messages != [@message]
        @failure_appendix = "(with message #{record.errors.full_messages.inspect})"
        return false
      end
    end

    true
  end

  description do
    description_prefix = "validate #{field} has a length of"
    conditions = []
    conditions << "at most #{@max_size}" if @max_size
    conditions << "with message #{@message.inspect}" if @message
    "#{description_prefix} #{conditions.join(', ')}"
  end

  failure_message { "expected to #{description}, but not#{@failure_appendix}" }
end

テストを実行すると

Blog::Post
  with validations
    should validate post_tags has a length of at most 5, with message "タグは5個までしか登録できません"

Finished in 0.24698 seconds (files took 5.23 seconds to load)
1 example, 0 failures

となって無事成功しました。