Skip to content

Latest commit

 

History

History
151 lines (117 loc) · 3.94 KB

rails-internals-inside-attribute-methods.md

File metadata and controls

151 lines (117 loc) · 3.94 KB

Rails Internals: Inside Active Model Attribute Methods

AttributeMethods is a useful class that alllows you to define dynamic prefixes/suffixes for your class attributes. Here's an example of what you can do with it:

require 'rubygems'
require 'active_model'

class Person
  include ActiveModel::AttributeMethods

  attr_accessor :name

  attribute_method_affix :prefix => 'reset_', :suffix => '_to_default'
  attribute_method_suffix '_present?'
  attribute_method_suffix '_contains?'
  define_attribute_methods %w{name}

  private

  def reset_attribute_to_default(attr)
    send("#{attr}=", nil)
  end

  def attribute_present?(attr)
    !send(attr).nil?
  end

  def attribute_contains?(attr, substring)
    send(attr).include?(substring)
  end

end

p = Person.new
p.name = "Oscar"
p.name_present? # true
p.name_contains?("Os") # true
p.reset_name_to_default
p.name_present? # false

Let's see how rails accomplishes this.

How methods are defined

There is a class named AttributeMethodMatcher which is used behind the scenes for storing attribute matchers. Relevant parts here:

class AttributeMethodMatcher
  attr_reader :prefix, :suffix, :method_missing_target

  AttributeMethodMatch = Struct.new(:target, :attr_name,
:method_name)

  def initialize(options = {})
    # other code

    @prefix, @suffix = options[:prefix] || '', options[:suffix] || ''
    @regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
    @method_missing_target = "#{@prefix}attribute#{@suffix}"
    @method_name = "#{prefix}%s#{suffix}"
  end

  def match(method_name)
    if @regex =~ method_name
      AttributeMethodMatch.new(method_missing_target, $1,
method_name)
    else
      nil
    end
  end

  def method_name(attr_name)
    @method_name % attr_name
  end
end

Every time you call either attribute_method_affix or attribute_method_prefix or attribute_method_suffix, you create an instance of this class which is then stored into an array. It's only when you call define_attribute_methods that the methods are really defined:

def define_attribute_methods(attr_names)
  attr_names.each { |attr_name| define_attribute_method(attr_name)
}
end

def define_attribute_method(attr_name)
  # Remember that attribute_method_matchers contains our original matchers
  attribute_method_matchers.each do |matcher|
    method_name = matcher.method_name(attr_name)

    unless instance_method_already_implemented?(method_name)
      generate_method =
"define_method_#{matcher.method_missing_target}"

      if respond_to?(generate_method, true)
        send(generate_method, attr_name)
      else
        define_optimized_call generated_attribute_methods,
method_name, matcher.method_missing_target, attr_name.to_s
      end
    end
  end
  attribute_method_matchers_cache.clear
end

# Removes all the previously dynamically defined methods from the class
def undefine_attribute_methods
  generated_attribute_methods.module_eval do
    instance_methods.each { |m| undef_method(m) }
  end
  attribute_method_matchers_cache.clear
end

# Returns true if the attribute methods defined have been generated.
def generated_attribute_methods #:nodoc:
  @generated_attribute_methods ||= begin
    mod = Module.new
    include mod
    mod
  end
end

Pay attention to the method definition of generated_attribute_methods:

@generated_attribute_methods ||= begin
  mod = Module.new
  include mod
  mod
end

What's happening here is that we're creating a module, including the module in our class, and then returning the module so we can keep track of it and dynamically add methods to it. Note that we can undefine those methods at any time by calling undefine_attribute_methods.

See for yourself

There's more happening behind the scenes:

ActiveModel::AttributeMethods on Github


@oscardelben