ActiveModel::APIを調べてみた
ActiveModel::Model
ActiveRecord::Base
と同じように,クラスをModelのように扱えるようにするmodule。
module ActiveModel module Model extend ActiveSupport::Concern include ActiveModel::API end end
実装は,extendとincludeを行っているだけだった。ActiveModel::API
の実装を見てみる。
ActiveModel::API
以下のModuleをincludeしている。
- ActiveModel::AttributeAssignment
- ActiveModel::Validations
- ActiveModel::Conversion
ActiveModel::API
でやっていることは,includeされたクラスのattributeを引数にinitializeできるようになる。以下のようにクラスを定義する。
class Human include ActiveModel::API attr_writer :first_name end
すると,以下のようにinitializeできる。
irb(main):208:0> human = Human.new(first_name: 'Taro') => #<Human:0x00007fa0072ca098 @first_name="Taro">
setterメソッドが定義されていれば,initializeする引数に追加できるようになる。ActiveRecordでも同様に,initializeの引数にattributeが追加できるが,この実装がどうなっているのか少し気になったので読んでみる。
/Users/yoshiokayuya/.rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/activemodel-7.0.2.2/lib/active_model/api.rb
def initialize(attributes = {}) assign_attributes(attributes) if attributes super() end
assign_attributes
の実装を見てみる。
def assign_attributes(new_attributes) unless new_attributes.respond_to?(:each_pair) raise ArgumentError, "When assigning attributes, you must pass a hash as an argument, #{new_attributes.class} passed." end return if new_attributes.empty? _assign_attributes(sanitize_for_mass_assignment(new_attributes)) end
initializeで渡された引数がeach_pair
を使える,StructやHash以外であれば例外,空であればメソッドが終了する。そして,渡された引数に対して,以下のsanitize_for_mass_assignment
が呼ばれている。
/Users/yoshiokayuya/.rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/activemodel-7.0.2.2/lib/active_model/forbidden_attributes_protection.rb
def sanitize_for_mass_assignment(attributes) if attributes.respond_to?(:permitted?) raise ActiveModel::ForbiddenAttributesError if !attributes.permitted? attributes.to_h else attributes end end
今回は,ActionController::Parameters
インスタンスではないため,attributes
をそのまま引数として,_assign_attributes
が呼び出される。
def _assign_attributes(attributes) attributes.each do |k, v| _assign_attribute(k, v) end end def _assign_attribute(k, v) setter = :"#{k}=" if respond_to?(setter) public_send(setter, v) else raise UnknownAttributeError.new(self, k.to_s) end end
あとは,send
を使って各attributesに対してsetterメソッドを呼ぶだけ。だから,attr_writer
が定義されているものさえあればinitializeの引数に加えることができる。また,setterが定義されていないものをinitialize時の引数に加えると
irb(main):213:0> human = Human.new(first_nam: 'Taro') /Users/yoshiokayuya/.rbenv/versions/3.0.2/lib/ruby/gems/3.0.0/gems/activemodel-7.0.2.2/lib/active_model/attribute_assignment.rb:51:in `_assign_attribute': unknown attribute 'first_nam' for Human. (ActiveModel::UnknownAttributeError)
というエラーをよく見かけるが,それは
raise UnknownAttributeError.new(self, k.to_s)
に起因するものだった。
UnknownAttributeError
話は逸れるが,UnknownAttributeError
classを読んでみた。
class UnknownAttributeError < NoMethodError attr_reader :record, :attribute def initialize(record, attribute) @record = record @attribute = attribute super("unknown attribute '#{attribute}' for #{@record.class}.") end end
initialize時の引数にrecord,attribute
をとる。先ほどのHuman
クラスの例では,record
にHuman
,attribute
にhoge
が当てはまる。そして,super
で,親のNoMethodError
クラスのinitialize時に,"unknown attribute '#{attribute}' for #{@record.class}."
というmsgが渡される。