LoginSignup
2
3

More than 3 years have passed since last update.

ActiveModelでenumを使いたい

Last updated at Posted at 2019-12-13

ActiveModel::Modelをincludeしたクラスにenumを使いたい場面が出てきたけども、
ActiveRecordしか使えなさそうだったのと、検索しても出てこなかったので、
moduleを作成しました。
ActiveModel::Attributesを使うのでrails 5.2以上の環境です。
未検証ですがそれ以下でも一応動くかもしれないです。

やっつけで作っているので、
enumで生成されるメソッドやオプション全てをカバーしていないのと、integerの項目にしか使えないです。

app/models/concerns/active_model/enum.rb
# frozen_string_literal: true

# ActiveModel::Attributesのattributeでenumを利用可能にするモジュール
# ただしintegerにしか使えない
# enum_helpのgemと同様に#{attribute}_i18nメソッドも追加する
module ActiveModel
  module Enum
    extend ActiveSupport::Concern

    module ClassMethods
      def enum(definitions)
        raise ArgumentError, 'enum attribute: { key: value, key, value, ...} の形式で指定してください' unless valid?(definitions)

        attribute = definitions.keys.first
        values = definitions.values.first

        # getterを上書き
        define_method(attribute.to_s) do
          values.invert[attributes[attribute.to_s]]
        end

        # 既存のsetterを別名に退避
        alias_method "#{attribute}_value=", "#{attribute}="

        # setterを上書き
        define_method("#{attribute}=") do |argument|
          checked_argument = case argument
                             when Integer
                               values.values.include?(argument) ? argument : nil
                             when String
                               values[argument.to_sym]
                             when Symbol
                               values[argument]
                             else
                               raise ArgumentError, 'string, symbol, integerのいずれかを指定してください'
                             end
          raise ArgumentError, "'#{argument}' is not a valid #{attribute}" if checked_argument.nil?

          send("#{attribute}_value=", checked_argument)
        end

        # enum_helpの_i18n的なメソッド追加
        define_method("#{attribute}_i18n") do
          enumed_value = send(attribute)
          I18n.t("enums.#{self.class.name.underscore}.#{attribute}.#{enumed_value}", default: enumed_value.to_s)
        end
      end

      private

      def valid?(definitions)
        return false unless definitions.is_a?(Hash)
        return false if definitions.keys.size != 1

        values = definitions.values.first
        return false unless values.is_a?(Hash)
        return false unless values.keys.all? { |key| key.is_a?(Symbol) }
        return false unless values.values.all? { |value| value.is_a?(Integer) }

        true
      end
    end
  end
end

使い方

app/models/sample.rb
class Sample
  include ActiveModel::Model
  include ActiveModel::Attributes
  include ActiveModel::Enum

  attribute :column_name, :integer

  enum column_name: { hoge: 1, piyo: 2, fuga: 3 }
end
config/locales/ja.yml
ja:
  enums:
    sample:
      column:
        hoge: ほげ
        piyo: ぴよ
        fuga: ふが

rails consoleで確認

irb(main):001:0> sample = Sample.new
=> #<Sample:0x000055ec983b15e8 @attributes=#<ActiveModel::AttributeSet:0x000055ec983b14a8 @attributes={"column"=>#<ActiveModel::Attribute::WithCastValue:0x000055ec983b1340 @name="column", @value_before_type_cast=nil, @type=#<ActiveModel::Type::Integer:0x00007f3e74051ea0 @precision=nil, @scale=nil, @limit=nil, @range=-2147483648...2147483648>, @original_attribute=nil>}>>
irb(main):002:0> sample.column = 1
=> 1
irb(main):003:0> sample.column
=> :hoge
irb(main):004:0> sample.column = 'piyo'
=> 'piyo'
irb(main):005:0> sample.column
=> :piyo
irb(main):006:0> sample.column_i18n
=> "ぴよ"
irb(main):007:0> sample.column = 4
Traceback (most recent call last):
        2: from (irb):6
        1: from app/models/concerns/active_model/enum.rb:37:in `block in enum'
ArgumentError ('4' is not a valid column)
2
3
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
2
3