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.
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
.
There's more happening behind the scenes:
ActiveModel::AttributeMethods on Github