Active Record attributesでDBレコードとモデルのアトリビュートの型を相互変換

# 動機
モデルのアトリビュートをDB上の型とは異なるもので扱いたい。
特に、アトリビュートが自作クラスのインスタンスとしてアクセスできたら便利な場面がある。例えば、

```
user = User.new

user.image = MyUserImageFile.new('img.jpg')
user.image.width   #=> 300
user.image.height  #=> 200

user.save   #=> imageカラムにファイル名が保存される
```

その他にも、
#### ファイルアップロード
	DB上はファイル名の文字列
	アトリビュートはFileオブジェクト
#### ユーザのメールアドレス
	DB上はメールアドレスの文字列
	アトリビュートは独自に作ったUserEmailオブジェクト
#### 日付の開始と終了
	DB上はそれぞれ別のカラム
	アトリビュートはRange
等々

Rails5にはActive Record attributesによって型変換する仕組みがあるため、これを利用する。

# 前提
Userはimageとemailを持つ。
imageはユーザがファイルアップロードする。
emailはユーザのメールアドレス。
```ruby
 # Table name: users
 #
 #  id              :integer          not null, primary key
 #  image           :string
 #  email           :string
```

# 構成
```
    app/model/user.rb
       /attributes/concrete_file.rb
                   concrete_class.rb
       /pseudo_models/user_email.rb
```
ConcreteFileは、ファイル名とFileオブジェクトの相互変換。
ConcreteClassは、任意のクラスとの相互変換。UserEmailで利用する。

# DB:ファイル名 <-> アトリビュート:File 

```ruby:app/attributes/concrete_file.rb
require 'pathname'
require 'fileutils'

# DB上ではファイル名、モデルではFileのインスタンスとして持つアトリビュート変換クラス
#
# class User
#   attribute :profile_image, ConcreteFile.new('path to file directory')
#
class ConcreteFile < ActiveRecord::Type::Value
  def initialize(dir)
    @dir = Pathname.new(dir)
    super()
  end

  def cast(value)
    case value
    when NilClass
      super
    when String
      # DB型の時はモデル型へ変換
      super( File.open(@dir + value) )
    when File
      # モデル型の時はそのまま
      super(value)
    else
      # モデル型の時はそのまま
      super(value)
    end
  end

  def serialize(value)
    return if value.nil?

    # DB型へ変換
    case value
    when ActionDispatch::Http::UploadedFile
      Fileutil.cp(value.tempfile.path, @dir + original_filename)
      super( original_filename )
    else
      super( File.basename value.to_path )
    end
  end
end
```
```ruby:app/models/user.rb
class User < ActiveRecord::Base
  ...
  attribute :image, ConcreteFile.new(public_path('upload_images'))
```

# DB:メールアドレス <-> アトリビュート:UserEmailオブジェクト
```ruby:
# DB上ではスキーマ定義型、モデルのアトリビュートでは指定したクラスの
# インスタンスとして持つアトリビュート変換クラス
#
# 指定するキャスト先クラスは、1引数のコンストラクタを持たなくてはならない。
#
# ex)
#   class User
#     attribute :email, ConcreteClass.new(:UserEmail)

class ConcreteClass < ActiveRecord::Type::Value
  def initialize(klass_name)
    @klass = klass_name.to_s.constantize
    super()
  end

  def type
    @klass.to_s.underscore
  end

  # attribute -> db
  def serialize(value)
    super( value.to )
  end

  # db -> attribute
  def deserialize(value)
    super(value)
  end

  private

    # user input -> attribute
    def cast_value(value)
      if value.kind_of? @klass
        value
      else
        @klass.new(value)
      end
    end
end
```
```ruby:app/pseudo_models/{email.rb,user_email.rb}
class Email
  include ActiveModel::Model

  attr_accessor :to
  alias_method :address, :to

  def initialize(attributes = {})
    case attributes
    when Hash
      super
    when Enumerable
      attribute_values = attributes.first(attribute_names.size)
      attributes = attribute_names.zip(attribute_values)

...

class UserEmail < Email
  mailer :UserMailer
end
```

```ruby:app/models/user.rb
class User < ActiveRecord::Base
  ...
  attribute :email, ConcreteClass.new(:UserEmail)
```

# その他
やり過ぎると混乱しそう。ほどほどに。