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