lib/active_model/attribute_methods.rb in activemodel-6.0.6.1 vs lib/active_model/attribute_methods.rb in activemodel-6.1.0.rc1
- old
+ new
@@ -205,14 +205,16 @@
# person.nickname # => "Bob"
# person.name_short? # => true
# person.nickname_short? # => true
def alias_attribute(new_name, old_name)
self.attribute_aliases = attribute_aliases.merge(new_name.to_s => old_name.to_s)
- attribute_method_matchers.each do |matcher|
- matcher_new = matcher.method_name(new_name).to_s
- matcher_old = matcher.method_name(old_name).to_s
- define_proxy_call false, self, matcher_new, matcher_old
+ CodeGenerator.batch(self, __FILE__, __LINE__) do |owner|
+ attribute_method_matchers.each do |matcher|
+ matcher_new = matcher.method_name(new_name).to_s
+ matcher_old = matcher.method_name(old_name).to_s
+ define_proxy_call false, owner, matcher_new, matcher_old
+ end
end
end
# Is +new_name+ an alias?
def attribute_alias?(new_name)
@@ -247,11 +249,13 @@
# def clear_attribute(attr)
# send("#{attr}=", nil)
# end
# end
def define_attribute_methods(*attr_names)
- attr_names.flatten.each { |attr_name| define_attribute_method(attr_name) }
+ CodeGenerator.batch(generated_attribute_methods, __FILE__, __LINE__) do |owner|
+ attr_names.flatten.each { |attr_name| define_attribute_method(attr_name, _owner: owner) }
+ end
end
# Declares an attribute that should be prefixed and suffixed by
# <tt>ActiveModel::AttributeMethods</tt>.
#
@@ -279,25 +283,27 @@
#
# person = Person.new
# person.name = 'Bob'
# person.name # => "Bob"
# person.name_short? # => true
- def define_attribute_method(attr_name)
- attribute_method_matchers.each do |matcher|
- method_name = matcher.method_name(attr_name)
+ def define_attribute_method(attr_name, _owner: generated_attribute_methods)
+ CodeGenerator.batch(_owner, __FILE__, __LINE__) do |owner|
+ 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.target}"
+ unless instance_method_already_implemented?(method_name)
+ generate_method = "define_method_#{matcher.target}"
- if respond_to?(generate_method, true)
- send(generate_method, attr_name.to_s)
- else
- define_proxy_call true, generated_attribute_methods, method_name, matcher.target, attr_name.to_s
+ if respond_to?(generate_method, true)
+ send(generate_method, attr_name.to_s, owner: owner)
+ else
+ define_proxy_call true, owner, method_name, matcher.target, attr_name.to_s
+ end
end
end
+ attribute_method_matchers_cache.clear
end
- attribute_method_matchers_cache.clear
end
# Removes all the previously dynamically defined methods from the class.
#
# class Person
@@ -321,16 +327,56 @@
# Person.undefine_attribute_methods
#
# person.name_short? # => NoMethodError
def undefine_attribute_methods
generated_attribute_methods.module_eval do
- instance_methods.each { |m| undef_method(m) }
+ undef_method(*instance_methods)
end
attribute_method_matchers_cache.clear
end
private
+ class CodeGenerator
+ class << self
+ def batch(owner, path, line)
+ if owner.is_a?(CodeGenerator)
+ yield owner
+ else
+ instance = new(owner, path, line)
+ result = yield instance
+ instance.execute
+ result
+ end
+ end
+ end
+
+ def initialize(owner, path, line)
+ @owner = owner
+ @path = path
+ @line = line
+ @sources = ["# frozen_string_literal: true\n"]
+ @renames = {}
+ end
+
+ def <<(source_line)
+ @sources << source_line
+ end
+
+ def rename_method(old_name, new_name)
+ @renames[old_name] = new_name
+ end
+
+ def execute
+ @owner.module_eval(@sources.join(";"), @path, @line - 1)
+ @renames.each do |old_name, new_name|
+ @owner.alias_method new_name, old_name
+ @owner.undef_method old_name
+ end
+ end
+ end
+ private_constant :CodeGenerator
+
def generated_attribute_methods
@generated_attribute_methods ||= Module.new.tap { |mod| include mod }
end
def instance_method_already_implemented?(method_name)
@@ -350,22 +396,18 @@
@attribute_method_matchers_cache ||= Concurrent::Map.new(initial_capacity: 4)
end
def attribute_method_matchers_matching(method_name)
attribute_method_matchers_cache.compute_if_absent(method_name) do
- # Bump plain matcher to last place so that only methods that do not
- # match any other pattern match the actual attribute name.
- # This is currently only needed to support legacy usage.
- matchers = attribute_method_matchers.partition(&:plain?).reverse.flatten(1)
- matchers.map { |matcher| matcher.match(method_name) }.compact
+ attribute_method_matchers.map { |matcher| matcher.match(method_name) }.compact
end
end
# Define a method `name` in `mod` that dispatches to `send`
# using the given `extra` args. This falls back on `define_method`
# and `send` if the given names cannot be compiled.
- def define_proxy_call(include_private, mod, name, target, *extra)
+ def define_proxy_call(include_private, code_generator, name, target, *extra)
defn = if NAME_COMPILABLE_REGEXP.match?(name)
"def #{name}(*args)"
else
"define_method(:'#{name}') do |*args|"
end
@@ -376,16 +418,15 @@
"#{"self." unless include_private}#{target}(#{extra})"
else
"send(:'#{target}', #{extra})"
end
- mod.module_eval <<-RUBY, __FILE__, __LINE__ + 1
- #{defn}
- #{body}
- end
- ruby2_keywords(:'#{name}') if respond_to?(:ruby2_keywords, true)
- RUBY
+ code_generator <<
+ defn <<
+ body <<
+ "end" <<
+ "ruby2_keywords(:'#{name}') if respond_to?(:ruby2_keywords, true)"
end
class AttributeMethodMatcher #:nodoc:
attr_reader :prefix, :suffix, :target
@@ -405,14 +446,10 @@
end
def method_name(attr_name)
@method_name % attr_name
end
-
- def plain?
- prefix.empty? && suffix.empty?
- end
end
end
# Allows access to the object attributes, which are held in the hash
# returned by <tt>attributes</tt>, as though they were first-class
@@ -497,22 +534,21 @@
# We are also defining a constant to hold the frozen string of
# the attribute name. Using a constant means that we do not have
# to allocate an object on each call to the attribute method.
# Making it frozen means that it doesn't get duped when used to
# key the @attributes in read_attribute.
- def self.define_attribute_accessor_method(mod, attr_name, writer: false)
+ def self.define_attribute_accessor_method(owner, attr_name, writer: false)
method_name = "#{attr_name}#{'=' if writer}"
if attr_name.ascii_only? && DEF_SAFE_NAME.match?(attr_name)
- yield method_name, "'#{attr_name}'.freeze"
+ yield method_name, "'#{attr_name}'"
else
safe_name = attr_name.unpack1("h*")
const_name = "ATTR_#{safe_name}"
const_set(const_name, attr_name) unless const_defined?(const_name)
temp_method_name = "__temp__#{safe_name}#{'=' if writer}"
attr_name_expr = "::ActiveModel::AttributeMethods::AttrNames::#{const_name}"
yield temp_method_name, attr_name_expr
- mod.alias_method method_name, temp_method_name
- mod.undef_method temp_method_name
+ owner.rename_method(temp_method_name, method_name)
end
end
end
end
end