lib/active_model/attribute_methods.rb in activemodel-6.1.7.10 vs lib/active_model/attribute_methods.rb in activemodel-7.0.0.alpha1
- old
+ new
@@ -65,10 +65,11 @@
module AttributeMethods
extend ActiveSupport::Concern
NAME_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?=]?\z/
CALL_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?]?\z/
+ FORWARD_PARAMETERS = "*args"
included do
class_attribute :attribute_aliases, instance_writer: false, default: {}
class_attribute :attribute_method_matchers, instance_writer: false, default: [ ClassMethods::AttributeMethodMatcher.new ]
end
@@ -103,12 +104,12 @@
# person = Person.new
# person.name = 'Bob'
# person.name # => "Bob"
# person.clear_name
# person.name # => nil
- def attribute_method_prefix(*prefixes)
- self.attribute_method_matchers += prefixes.map! { |prefix| AttributeMethodMatcher.new prefix: prefix }
+ def attribute_method_prefix(*prefixes, parameters: nil)
+ self.attribute_method_matchers += prefixes.map! { |prefix| AttributeMethodMatcher.new(prefix: prefix, parameters: parameters) }
undefine_attribute_methods
end
# Declares a method available for all attributes with the given suffix.
# Uses +method_missing+ and <tt>respond_to?</tt> to rewrite the method.
@@ -138,12 +139,12 @@
#
# person = Person.new
# person.name = 'Bob'
# person.name # => "Bob"
# person.name_short? # => true
- def attribute_method_suffix(*suffixes)
- self.attribute_method_matchers += suffixes.map! { |suffix| AttributeMethodMatcher.new suffix: suffix }
+ def attribute_method_suffix(*suffixes, parameters: nil)
+ self.attribute_method_matchers += suffixes.map! { |suffix| AttributeMethodMatcher.new(suffix: suffix, parameters: parameters) }
undefine_attribute_methods
end
# Declares a method available for all attributes with the given prefix
# and suffix. Uses +method_missing+ and <tt>respond_to?</tt> to rewrite
@@ -175,11 +176,11 @@
# person = Person.new
# person.name # => 'Gem'
# person.reset_name_to_default!
# person.name # => 'Default Name'
def attribute_method_affix(*affixes)
- self.attribute_method_matchers += affixes.map! { |affix| AttributeMethodMatcher.new prefix: affix[:prefix], suffix: affix[:suffix] }
+ self.attribute_method_matchers += affixes.map! { |affix| AttributeMethodMatcher.new(**affix) }
undefine_attribute_methods
end
# Allows you to make aliases for attributes.
#
@@ -205,15 +206,37 @@
# 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)
- CodeGenerator.batch(self, __FILE__, __LINE__) do |owner|
+ CodeGenerator.batch(self, __FILE__, __LINE__) do |code_generator|
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
+ method_name = matcher.method_name(new_name).to_s
+ target_name = matcher.method_name(old_name).to_s
+ parameters = matcher.parameters
+
+ mangled_name = target_name
+ unless NAME_COMPILABLE_REGEXP.match?(target_name)
+ mangled_name = "__temp__#{target_name.unpack1("h*")}"
+ end
+
+ code_generator.define_cached_method(method_name, as: mangled_name, namespace: :alias_attribute) do |batch|
+ body = if CALL_COMPILABLE_REGEXP.match?(target_name)
+ "self.#{target_name}(#{parameters || ''})"
+ else
+ call_args = [":'#{target_name}'"]
+ call_args << parameters if parameters
+ "send(#{call_args.join(", ")})"
+ end
+
+ modifier = matcher.parameters == FORWARD_PARAMETERS ? "ruby2_keywords " : ""
+
+ batch <<
+ "#{modifier}def #{mangled_name}(#{parameters || ''})" <<
+ body <<
+ "end"
+ end
end
end
end
# Is +new_name+ an alias?
@@ -294,11 +317,11 @@
generate_method = "define_method_#{matcher.target}"
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
+ define_proxy_call(owner, method_name, matcher.target, matcher.parameters, attr_name.to_s, namespace: :active_model)
end
end
end
attribute_method_matchers_cache.clear
end
@@ -333,11 +356,41 @@
end
attribute_method_matchers_cache.clear
end
private
- class CodeGenerator
+ class CodeGenerator # :nodoc:
+ class MethodSet
+ METHOD_CACHES = Hash.new { |h, k| h[k] = Module.new }
+
+ def initialize(namespace)
+ @cache = METHOD_CACHES[namespace]
+ @sources = []
+ @methods = {}
+ end
+
+ def define_cached_method(name, as: name)
+ name = name.to_sym
+ as = as.to_sym
+ @methods.fetch(name) do
+ unless @cache.method_defined?(as)
+ yield @sources
+ end
+ @methods[name] = as
+ end
+ end
+
+ def apply(owner, path, line)
+ unless @sources.empty?
+ @cache.module_eval("# frozen_string_literal: true\n" + @sources.join(";"), path, line)
+ end
+ @methods.each do |name, as|
+ owner.define_method(name, @cache.instance_method(as))
+ end
+ end
+ end
+
class << self
def batch(owner, path, line)
if owner.is_a?(CodeGenerator)
yield owner
else
@@ -351,27 +404,20 @@
def initialize(owner, path, line)
@owner = owner
@path = path
@line = line
- @sources = ["# frozen_string_literal: true\n"]
- @renames = {}
+ @namespaces = Hash.new { |h, k| h[k] = MethodSet.new(k) }
end
- def <<(source_line)
- @sources << source_line
+ def define_cached_method(name, namespace:, as: name, &block)
+ @namespaces[namespace].define_cached_method(name, as: as, &block)
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
+ @namespaces.each_value do |method_set|
+ method_set.apply(@owner, @path, @line - 1)
end
end
end
private_constant :CodeGenerator
@@ -396,46 +442,52 @@
@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
- attribute_method_matchers.map { |matcher| matcher.match(method_name) }.compact
+ attribute_method_matchers.filter_map { |matcher| matcher.match(method_name) }
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, code_generator, name, target, *extra)
- defn = if NAME_COMPILABLE_REGEXP.match?(name)
- "def #{name}(*args)"
- else
- "define_method(:'#{name}') do |*args|"
+ # using the given `extra` args. This falls back on `send`
+ # if the called name cannot be compiled.
+ def define_proxy_call(code_generator, name, target, parameters, *call_args, namespace:)
+ mangled_name = name
+ unless NAME_COMPILABLE_REGEXP.match?(name)
+ mangled_name = "__temp__#{name.unpack1("h*")}"
end
- extra = (extra.map!(&:inspect) << "*args").join(", ")
+ code_generator.define_cached_method(name, as: mangled_name, namespace: namespace) do |batch|
+ call_args.map!(&:inspect)
+ call_args << parameters if parameters
- body = if CALL_COMPILABLE_REGEXP.match?(target)
- "#{"self." unless include_private}#{target}(#{extra})"
- else
- "send(:'#{target}', #{extra})"
- end
+ body = if CALL_COMPILABLE_REGEXP.match?(target)
+ "self.#{target}(#{call_args.join(", ")})"
+ else
+ call_args.unshift(":'#{target}'")
+ "send(#{call_args.join(", ")})"
+ end
- code_generator <<
- defn <<
- body <<
- "end" <<
- "ruby2_keywords(:'#{name}') if respond_to?(:ruby2_keywords, true)"
+ modifier = parameters == FORWARD_PARAMETERS ? "ruby2_keywords " : ""
+
+ batch <<
+ "#{modifier}def #{mangled_name}(#{parameters || ''})" <<
+ body <<
+ "end"
+ end
end
- class AttributeMethodMatcher #:nodoc:
- attr_reader :prefix, :suffix, :target
+ class AttributeMethodMatcher # :nodoc:
+ attr_reader :prefix, :suffix, :target, :parameters
AttributeMethodMatch = Struct.new(:target, :attr_name)
- def initialize(options = {})
- @prefix, @suffix = options.fetch(:prefix, ""), options.fetch(:suffix, "")
+ def initialize(prefix: "", suffix: "", parameters: nil)
+ @prefix = prefix
+ @suffix = suffix
+ @parameters = parameters.nil? ? FORWARD_PARAMETERS : parameters
@regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
@target = "#{@prefix}attribute#{@suffix}"
@method_name = "#{prefix}%s#{suffix}"
end
@@ -467,11 +519,11 @@
else
match = matched_attribute_method(method.to_s)
match ? attribute_missing(match, *args, &block) : super
end
end
- ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
+ ruby2_keywords(:method_missing)
# +attribute_missing+ is like +method_missing+, but for attributes. When
# +method_missing+ is called we check to see if there is a matching
# attribute method. If so, we tell +attribute_missing+ to dispatch the
# attribute. This method can be overloaded to customize the behavior.
@@ -518,14 +570,10 @@
module AttrNames # :nodoc:
DEF_SAFE_NAME = /\A[a-zA-Z_]\w*\z/
# We want to generate the methods via module_eval rather than
# define_method, because define_method is slower on dispatch.
- # Evaluating many similar methods may use more memory as the instruction
- # sequences are duplicated and cached (in MRI). define_method may
- # be slower on dispatch, but if you're careful about the closure
- # created, then define_method will consume much less memory.
#
# But sometimes the database might return columns with
# characters that are not allowed in normal method names (like
# 'my_column(omg)'. So to work around this we first define with
# the __temp__ identifier, and then use alias method to rename
@@ -545,10 +593,9 @@
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
- owner.rename_method(temp_method_name, method_name)
end
end
end
end
end