Upgrade to Pro — share decks privately, control downloads, hide ads and more …

ActiveModel::APIで使用されるゲッター・セッターメソッドについて

Akimu Hamaguchi
January 11, 2024
58

 ActiveModel::APIで使用されるゲッター・セッターメソッドについて

Akimu Hamaguchi

January 11, 2024
Tweet

Transcript

  1. ActiveModel::API でできること include することでPORO (Plain Old Ruby Object )をモデルっぽく扱うことができる。 ActiveModel::Validations

    (検証) ActiveModel::Conversion (変換) ActiveModel::Translation (翻訳) その他 class User include ActiveModel::API attr_accessor :name validates :name, presence: true end user = User.new(name: " 太郎") # 検証 user.valid? #=> true # 変換 user.to_model == user #=> true # 翻訳 User.human_attribute_name(:name) #=> Name
  2. この発表でわかること 初期化時にセッターメソッドが呼び出されるまでの処理の流れ class User include ActiveModel::API def name=(name) @name =

    " 花子" end end User.new(name: ' 太郎') #=> #<User:0x0000000106c00ab0 @name=" 花子"> ゲッターメソッドがバリデーションに使われるまでの流れ class User include ActiveModel::API validates :name, presence: true def name " 花子" end end user = User.new() user.valid? #=> true
  3. ActiveModel::API で initialize メソッドが定義されている class User include ActiveModel::API attr_writer :name,

    :age end User.new(name: ' 太郎', age: 20) #=> #<User:0x0000000107a59e40 @age=20, @name=" 太郎"> module ActiveModel module API def initialize(attributes = {}) assign_attributes(attributes) if attributes super() end end end
  4. ActiveModel::AttributeAssignment#assign_attributes でセッターメソッドを呼ぶ module ActiveModel module AttributeAssignment 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 alias attributes= assign_attributes private 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) # User.new(name: " 太郎") の場合 # public_send(:name=, ' 太郎') public_send(setter, v) else raise UnknownAttributeError.new(self, k.to_s) end end end end
  5. オブジェクト初期化時にwriter メソッドを使用するまでの流れ ActiveModel::AttributeAssignment をinclude した ActiveModel::API が initialize メソッド内で assign_attributes

    を呼ぶ ActiveModel::AttributeAssignment が assign_attributes メソッドを提供する インスタンス初期化時に渡される、ハッシュのkey と同じ名前のセッターメソッドを呼ぶ セッターメソッドが使われる例 class User include ActiveModel::API def name=(name) @name = " 花子" end end User.new(name: ' 太郎') #=> #<User:0x0000000106c00ab0 @name=" 花子">
  6. バリデーションの際にゲッターメソッドが利用される バリデーションを通るゲッターメソッドを作ると valid? が true を返す class User include ActiveModel::API

    validates :name, presence: true def name " 太郎" end end user = User.new() user.valid? #=> true ゲッターメソッドを削除するとエラーが出る class User include ActiveModel::API validates :name, presence: true end user = User.new() user.valid? # activemodel-7.1.2/lib/active_model/validator.rb:152:in `block in validate': # undefined method `name' for #<User:0x0000000104f748d8 @validation_context=nil, @errors=#<ActiveModel::Errors []>> (NoMethodError) # value = record.read_attribute_for_validation(attribute)
  7. ActiveModel::Validations#valid? def valid?(context = nil) current_context, self.validation_context = validation_context, context

    errors.clear run_validations! ensure self.validation_context = current_context end private def run_validations! _run_validate_callbacks errors.empty? end
  8. run_validate_callbacks # ActiveModel::Validations define_callbacks :validate, scope: :name def define_callbacks(*names) options

    = names.extract_options! names.each do |name| name = name.to_sym ([self] + self.descendants).each do |target| target.set_callbacks name, CallbackChain.new(name, options) end module_eval <<-RUBY, __FILE__, __LINE__ + 1 def _run_#{name}_callbacks(&block) run_callbacks #{name.inspect}, &block end 動的にメソッドを定義している
  9. ActiveSupport::Callbacks#run_callbacks def run_callbacks(kind, type = nil) callbacks = __callbacks[kind.to_sym] if

    callbacks.empty? yield if block_given? else env = Filters::Environment.new(self, false, nil) next_sequence = callbacks.compile(type) # Common case: no 'around' callbacks defined if next_sequence.final? next_sequence.invoke_before(env) env.value = !env.halted && (!block_given? || yield) next_sequence.invoke_after(env) env.value else # 省略 end end end
  10. ActiveSupport::Callbacks::CallbackChain#compile def compile(type) if type.nil? @all_callbacks || @mutex.synchronize do final_sequence

    = CallbackSequence.new @all_callbacks ||= @chain.reverse.inject(final_sequence) do |callback_sequence, callback| callback.apply(callback_sequence) end end else # 省略 end end # @chain の中身 User.__callbacks[:validate].instance_variable_get(:@chain) #=> [#<ActiveSupport::Callbacks::Callback:0x0000000105871418 @chain_config={:scope=>:name, :terminator=>#<Proc:0x0000000103e86800 .../active_support/callbacks.rb:710>}, @filter=#<ActiveModel::Validations::PresenceValidator:0x0000000104ccd968 @attributes=[:name], @options={}>, @if=[], @kind=:before, @name=:validate, @unless=[]>]
  11. ActiveSupport::Callbacks::Callback#apply def apply(callback_sequence) user_conditions = conditions_lambdas user_callback = CallTemplate.build(@filter, self)

    case kind when :before Filters::Before.build(callback_sequence, user_callback.make_lambda, user_conditions, chain_config, @filter, name) when :after Filters::After.build(callback_sequence, user_callback.make_lambda, user_conditions, chain_config) when :around callback_sequence.around(user_callback, user_conditions) end end self #=> <ActiveSupport::Callbacks::Callback:0x0000000105871418 @chain_config={:scope=>:name, :terminator=>#<Proc:0x0000000103e86800 .../active_support/callbacks.rb:710>}, @filter=#<ActiveModel::Validations::PresenceValidator:0x0000000104ccd968 @attributes=[:name], @options={}>, @if=[], @kind=:before, @name=:validate, @unless=[]>
  12. ActiveSupport::Callbacks::CallTemplate#build def self.build(filter, callback) case filter when Symbol MethodCall.new(filter) when

    Conditionals::Value ProcCall.new(filter) when ::Proc if filter.arity > 1 InstanceExec2.new(filter) elsif filter.arity > 0 InstanceExec1.new(filter) else InstanceExec0.new(filter) end else # 第一引数はActiveModel::Validations::PresenceValidator のインスタンス # 第二引数は:validate ObjectCall.new(filter, callback.current_scopes.join("_").to_sym) end end filter # => #<ActiveModel::Validations::PresenceValidator:0x0000000104ccd968 @attributes=[:name], @options={}> callback.current_scopes.join("_").to_sym #=> :validate
  13. ActiveModel::Validations::PresenceValidator#validate が呼び出される 第一引数にレシーバー・第二引数に呼び出したいメソッドのシンボルを入れることで将来呼び出すことができる class Hoge def self.say(value) puts value end

    end callback = ActiveSupport::Callbacks::CallTemplate::ObjectCall.new(Hoge, :say).make_lambda callback.call('hello', nil) # => hello ObjectCall.new(filter, callback.current_scopes.join("_").to_sym) # 第一引数はActiveModel::Validations::PresenceValidator のインスタンス # => #<ActiveModel::Validations::PresenceValidator:0x0000000104ccd968 @attributes=[:name], @options={}> # 第二引数は:validate #callback.current_scopes.join("_").to_sym #=> :validate
  14. ゲッターメソッドが呼び出されることがわかった def run_callbacks(kind, type = nil) callbacks = __callbacks[kind.to_sym] if

    callbacks.empty? yield if block_given? else env = Filters::Environment.new(self, false, nil) next_sequence = callbacks.compile(type) # Common case: no 'around' callbacks defined if next_sequence.final? # Proc をコールする時の引数にUser のインスタンスが入る next_sequence.invoke_before(env) env.value = !env.halted && (!block_given? || yield) next_sequence.invoke_after(env) env.value # 省略 def validate(record) attributes.each do |attribute| # User#send(:name) value = record.read_attribute_for_validation(attribute) next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank]) value = prepare_value_for_validation(value, record, attribute) validate_each(record, attribute, value) end end # ActiveModel::Validations alias :read_attribute_for_validation :send