RailsのEnumで複数項目を選択可能にするGemを作ってる話

## つまり・・・どういうこと？

一言で言うと、「enumで複数項目を選択できる風にするRails Plugin」。

リポジトリ(未完成。gemには登録してないので、installするにはgithubから)
https://github.com/EastResident/any_enums

何を言ってるのか分からないと思うので、以下のサンプルコードで雰囲気を感じて下さい

```rb
class Engineer < ApplicationRecord
  include AnyEnums
  any_enums field: { server: 1, front: 2, infra: 3 }
end

engineer = Engineer.server_or_front.new

p engineer.field # => "server_or_front"
p engineer.fields # => ["server", "front"]
```

## 開発思想

例えば、 ApplicationRecordを継承するEngineerモデルがあるとしましょう。Engineerモデルはfieldと言う属性を持っており、これはserver、front、infraのいずれかの値が入ります(説明するまでもないですが、大雑把な専門分野のイメージです)。
Engineerのインスタンスはfieldを一つだけではなく、複数持つ場合もあるとします(フルスタックエンジニアとかあるしね)。
このようなデータ構造を持たせる場合、一般的にRailsなら中間テーブルを挟んだ多対多のリレーションを作るかもしれません。

```rb
class Engineer < ApplicationRecord
  has_many :engineer_fields
  has_many :fields, through: :engineer_fields
end

class Field < ApplicationRecord
  has_many :engineer_fields
  has_many :engineers, through: :engineer_fields
end

class EngineerField < ApplicationRecord
  belongs_to :engineer
  belongs_to :field
end

```

field属性が今後も増えていくなら、この構造は合理的だと思います。
しかし、field属性の種類が今後も増える予定がなく、3項目のままだとしたらどうでしょうか？

Engineerモデルにfield属性を持たせるために、モデル(とテーブル)を二つも増やすのは少し大げさな気もします。

このようなケースを解決する選択肢の一つを提供するのが、このany_enumsです。


# 使い方


## 定義

普通、enumは以下のように定義すると思います。


### 普通のenum定義

```rb
class Engineer < ApplicationRecord
  enum field: { server: 1, front: 2, infra: 3 }
end
```

any_enumsでは、これを以下のように変更します。


### any_enumsの定義

```rb
class Engineer < ApplicationRecord
  include AnyEnums
  any_enums field: { server: 1, front: 2, infra: 3 }
end
```

AnyEnumsをincludeして、enumをany_enumsに置き換えるだけですね。


## インスタンスの作成

例えば、通常のenumでfieldがserverのEngineerモデルのインスタンスを作成するには以下のようにします。

```rb
engineer = Engineer.server.new
engineer.save
```

そして、fieldの値を取得する場合はこんな感じですね。

```rb
p engineer.field # => "server"
p engineer.field_before_type_cast # => 1
```

では次にany_enumsの機能を使って、serverとfront両方をfieldに持つEngineerのインスタンスを作成します。

```rb
engineer = Engineer.server_or_front.new
engineer.save
```

ここでengineerインスタンスは、fieldメソッド・field_before_type_castメソッドの他に、fields・fields_before_type_castメソッドが使えるようになっています

```rb
p engineer.field # => "server_or_front"
p engineer.fields # => ["server", "front"]

p engineer.field_before_type_cast # => 110
p engineer.fields_before_type_cast # => [1, 2]
```

先ほどインスタンスを作成する際に、server_or_front.newとしましたが、これはfront_or_server.newとしても動作します(ただし、enumの定義順にorで繋ぐことが推奨されます)。
どちらの方法でインスタンスを作成しても、fieldに入る値は同じになります。


### 宣言はどちらでもOKだが、入る値は同じ

```rb
engineer = Engineer.server_or_front.new
engineer = Engineer.front_or_server.new
```

# 仕組み

前項で使い方を説明した際に、以下のような記述をしました。

```rb
engineer = Engineer.server_or_front.new
p engineer.field # => "server_or_front"
p engineer.field_before_type_cast # => 110
```

お気付きだと思いますが、any_enumsは裏側で{server_or_front: 110}と言うような割り当てを行っています。では、どのようなルールでserver_or_frontが110に割り当てられたのでしょうか？

any_enumsは独自のハッシュテーブルを保持しており、0〜9の整数からなる全ての組み合わせに対して、101以上の整数を割り当てています。

```
[0, 1]=>101,
[0, 2]=>102,
[0, 3]=>103,
〜〜
[4, 6, 7, 8]=>467,
[4, 6, 7, 9]=>468,
〜〜
〜〜
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] => 1113
```

御察しの通り、上記の機構を持つ理由からany_enumsを使用するには制限があります。
まず、any_enumsには0から9までの整数しか割り当てられません。また、101から1113までの整数が自動的に割り当てられる可能性があるため、割り当てが非推奨になります。このような仕様にした理由としては、自分の経験から、一つのカラムに対して10以上の値を列挙することはないと判断したからです(経験上、多くて5〜6個)。

## 今後の展望

- formで利用するview helperの作成
- 通常のenumで使える機能を可能な限り再現