1. kano-e

    No comment

    kano-e
Changes in body
Source | HTML | Preview
@@ -1,208 +1,211 @@
Rails 4.0.2 です。
auto increment な id 以外に、ユーザが自分で設定できるユニークな文字列のカラムを用意したい、みたいな時に、用意されているValidatorを組み合わせて、以下のようにモデルに設定する。
-```ruby:app/models/user.rb
+```ruby:
class User < ActiveRecord::Base
validates :username,
presence: true, # 必須にしたい!
uniqueness: true, # URLに使うしユニーク!
length: { maximum: 16 }, # あんまり長いのも……
format: { with: /\A[a-z0-9]+\z/i } # やっぱり半角英数字のみだよね!
end
```
これが、モデル一個だけなら良いのだけれど、複数のモデルで似たようなことをやろうとすると、面倒だったりする。
-```ruby:app/models/item.rb
+```ruby:
class Item < ActiveRecord::Base
# なんだか User と似たようなこと書いてるなー……
validates :name,
presence: true,
uniqueness: { scope: :user_id },
length: { maximum: 32 },
format: { with: /\A[a-z0-9]+\z/i }
end
```
なんとか `format` 部分だけ Validator にしたとしても、その手間の割に楽になっている気がしない。
-```ruby:app/validators/name_format_validator.rb
+```ruby:
class NameFormatValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless /\A[a-z0-9]+\z/i === value
record.errors[attribute] << (options[:message] || "invalid")
end
end
end
-```
-```ruby:app/model/user.rb
class User < ActiveRecord::Base
# ちっとも楽になった気がしない……
validates :username,
presence: true,
uniqueness: true,
length: { maximum: 16 },
name_format: true
end
```
理想はこういう形。
-```ruby:app/models/user.rb
+```ruby:
class User < ActiveRecord::Base
validates :username, name_format: { maximum: 16 }
end
```
-```ruby:app/models/item.rb
+```ruby:
class Item < ActiveRecord::Base
validates :name, name_format: { scope: :user_id, maximum: 32 }
end
```
なので、全部まとめて Validator にしようかと思ったけど、`#validate_each` の中にこのチェック全部入れるの? と手が止まってしまう。
-```ruby:app/validators/name_format_validator.rb
+```ruby:
class NameFormatValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
# 空だったらチェックして……?
if value.blank?
# あれ、空の時のエラーメッセージなんだっけ?
end
# ユニークかどうかって Validator の中からどうやるんだっけ?
# ...
end
end
```
そもそも、その辺りの仕組みはすでにあるのに、`#validate_each` の中に再実装しないといけないっていうのが間違ってる!
じゃあ、そもそもすでにある Validator ってどうなってるの?
というわけで `ActiveModel::Validations` の中身を確認してみた。
`ActiveModel::Validations` は `ActiveModel::Model` に `include` されている。
参考 ⇒ http://apidock.com/rails/ActiveModel/Validations
`ActiveModel::Validations::ClassMethods` の中に `.validates` が定義されている。
```ruby:activemodel/lib/active_model/validations/validates.rb
def validates(*attributes)
defaults = attributes.extract_options!.dup
validations = defaults.slice!(*_validates_default_keys)
raise ArgumentError, "You need to supply at least one attribute" if attributes.empty?
raise ArgumentError, "You need to supply at least one validation" if validations.empty?
defaults[:attributes] = attributes
validations.each do |key, options|
next unless options
key = "#{key.to_s.camelize}Validator"
begin
validator = key.include?('::') ? key.constantize : const_get(key)
rescue NameError
raise ArgumentError, "Unknown validator: '#{key}'"
end
validates_with(validator, defaults.merge(_parse_validates_options(options)))
end
end
```
色々やっているけれども、対象のattribute名、Validatorの名前とそのoprions、に分解しValidatorクラスを見つけて `.validates_with` に渡している。
```ruby:activemodel/lib/active_model/validations/with.rb
def validates_with(*args, &block)
options = args.extract_options!
args.each do |klass|
validator = klass.new(options, &block)
validator.setup(self) if validator.respond_to?(:setup)
if validator.respond_to?(:attributes) && !validator.attributes.empty?
validator.attributes.each do |attribute|
_validators[attribute.to_sym] << validator
end
else
_validators[nil] << validator
end
validate(validator, options)
end
end
```
`.validates_with` の中では Validator クラスのインスタンスを作っている。
そのインスタンス作成直後に `validator.setup(self) if validator.respond_to?(:setup)` ということをしている。
どうやら Validator クラスに `#setup` というメソッドがあれば、それを呼ぶらしい。
試しにいくつかの Validator クラスを見てみたけれども `#setup` メソッドを持っているものはそんなにない。
`ActiveModel::Validations::ConfirmationValidator` にあったので、どんな感じか見てみる。
```ruby:activemodel/lib/active_model/validations/confirmation.rb
def setup(klass)
klass.send(:attr_reader, *attributes.map do |attribute|
:"#{attribute}_confirmation" unless klass.method_defined?(:"#{attribute}_confirmation")
end.compact)
klass.send(:attr_writer, *attributes.map do |attribute|
:"#{attribute}_confirmation" unless klass.method_defined?(:"#{attribute}_confirmation=")
end.compact)
end
```
klass を受け取って、その klass に対してあらかじめ何か操作をしたい時に使えるらしい。
いくつかコードを確認して、 `attributes` と `options` が使えることがわかったので、以下のように書ける。
-```ruby:app/validators/name_format_validator.rb
+```ruby:
class NameFormatValidator < ActiveModel::EachValidator
def setup(klass)
klass.validates(
*attributes,
presence: true,
uniqueness: options[:scope].present? ? options.slice(:scope) : true,
length: options.slice(:in, :minimum, :maximum, :is),
)
end
def validate_each(record, attribute, value); end
end
```
これで
-```ruby:app/models/item.rb
+```ruby:
class Item < ActiveRecord::Base
validates :name, name_format: { scope: :user_id, maximum: 32 }
end
```
これが実現できる!
と思ったけど Concern でも良かったかなーって、書きながら思い付いた。
-```ruby:app/models/concerns/name_format_column.rb
+```ruby:
module NameFormatColumn
extend ActiveSupport::Concern
module Class Methods
def name_format_column(*attributes)
options = attributes.extract_options!
send(
:validates,
*attributes,
presence: true,
uniqueness: options[:scope].present? ? options.slice(:scope) : true,
length: options.slice(:in, :minimum, :maximum, :is),
)
end
end
end
+
+class Item < ActiveRecord::Base
+ include NameFormatColumn
+ name_format_column :name, scope: :user_id, muximum: 32
+end
```
今思い付いて書き下したコードなので、これで動くかはわかんない。