lib/active_model/attribute_methods.rb in activemodel-7.1.5 vs lib/active_model/attribute_methods.rb in activemodel-7.2.0.beta1

- old
+ new

@@ -64,11 +64,10 @@ 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_patterns, instance_writer: false, default: [ ClassMethods::AttributeMethodPattern.new ] end @@ -213,45 +212,27 @@ ActiveSupport::CodeGenerator.batch(generated_attribute_methods, __FILE__, __LINE__) do |code_generator| generate_alias_attribute_methods(code_generator, new_name, old_name) end end - def generate_alias_attribute_methods(code_generator, new_name, old_name) # :nodoc: - ActiveSupport::CodeGenerator.batch(code_generator, __FILE__, __LINE__) do |owner| - attribute_method_patterns.each do |pattern| - alias_attribute_method_definition(code_generator, pattern, new_name, old_name) - end - attribute_method_patterns_cache.clear + def generate_alias_attribute_methods(code_generator, new_name, old_name) + attribute_method_patterns.each do |pattern| + alias_attribute_method_definition(code_generator, pattern, new_name, old_name) end end def alias_attribute_method_definition(code_generator, pattern, new_name, old_name) # :nodoc: method_name = pattern.method_name(new_name).to_s target_name = pattern.method_name(old_name).to_s parameters = pattern.parameters - mangled_name = target_name - unless NAME_COMPILABLE_REGEXP.match?(target_name) - mangled_name = "__temp__#{target_name.unpack1("h*")}" - end + mangled_name = build_mangled_name(target_name) - code_generator.define_cached_method(mangled_name, as: method_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 + call_args = [] + call_args << parameters if parameters - modifier = parameters == FORWARD_PARAMETERS ? "ruby2_keywords " : "" - - batch << - "#{modifier}def #{mangled_name}(#{parameters || ''})" << - body << - "end" - end + define_call(code_generator, method_name, target_name, mangled_name, parameters, call_args, namespace: :alias_attribute) end # Is +new_name+ an alias? def attribute_alias?(new_name) attribute_aliases.key? new_name.to_s @@ -322,48 +303,29 @@ # # person = Person.new # person.name = 'Bob' # person.name # => "Bob" # person.name_short? # => true - def define_attribute_method(attr_name, _owner: generated_attribute_methods, as: attr_name) + def define_attribute_method(attr_name, _owner: generated_attribute_methods) ActiveSupport::CodeGenerator.batch(_owner, __FILE__, __LINE__) do |owner| attribute_method_patterns.each do |pattern| - define_attribute_method_pattern(pattern, attr_name, owner: owner, as: as) + method_name = pattern.method_name(attr_name) + + unless instance_method_already_implemented?(method_name) + generate_method = "define_method_#{pattern.proxy_target}" + + if respond_to?(generate_method, true) + send(generate_method, attr_name.to_s, owner: owner) + else + define_proxy_call(owner, method_name, pattern.proxy_target, pattern.parameters, attr_name.to_s, namespace: :active_model_proxy) + end + end end attribute_method_patterns_cache.clear end end - def define_attribute_method_pattern(pattern, attr_name, owner:, as:, override: false) # :nodoc: - canonical_method_name = pattern.method_name(attr_name) - public_method_name = pattern.method_name(as) - - # If defining a regular attribute method, we don't override methods that are explictly - # defined in parrent classes. - if instance_method_already_implemented?(public_method_name) - # However, for `alias_attribute`, we always define the method. - # We check for override second because `instance_method_already_implemented?` - # also check for dangerous methods. - return unless override - end - - generate_method = "define_method_#{pattern.proxy_target}" - if respond_to?(generate_method, true) - send(generate_method, attr_name.to_s, owner: owner, as: as) - else - define_proxy_call( - owner, - canonical_method_name, - pattern.proxy_target, - pattern.parameters, - attr_name.to_s, - namespace: :active_model_proxy, - as: public_method_name, - ) - end - end - # Removes all the previously dynamically defined methods from the class, including alias attribute methods. # # class Person # include ActiveModel::AttributeMethods # @@ -401,10 +363,12 @@ private def inherited(base) # :nodoc: super base.class_eval do @attribute_method_patterns_cache = nil + @aliases_by_attribute_name = nil + @generated_attribute_methods = nil end end def resolve_attribute_name(name) attribute_aliases.fetch(super, &:itself) @@ -438,32 +402,41 @@ end # Define a method `name` in `mod` that dispatches to `send` # using the given `extra` args. This falls back on `send` # if the called name cannot be compiled. - def define_proxy_call(code_generator, name, proxy_target, parameters, *call_args, namespace:, as: name) + def define_proxy_call(code_generator, name, proxy_target, parameters, *call_args, namespace:) + mangled_name = build_mangled_name(name) + + call_args.map!(&:inspect) + call_args << parameters if parameters + namespace = :"#{namespace}_#{proxy_target}_#{call_args.join("_")}}" + + define_call(code_generator, name, proxy_target, mangled_name, parameters, call_args, namespace: namespace) + end + + def build_mangled_name(name) mangled_name = name + unless NAME_COMPILABLE_REGEXP.match?(name) mangled_name = "__temp__#{name.unpack1("h*")}" end - call_args.map!(&:inspect) - call_args << parameters if parameters - namespace = :"#{namespace}_#{proxy_target}" + mangled_name + end - code_generator.define_cached_method(mangled_name, as: as, namespace: namespace) do |batch| - body = if CALL_COMPILABLE_REGEXP.match?(proxy_target) - "self.#{proxy_target}(#{call_args.join(", ")})" + def define_call(code_generator, name, target_name, mangled_name, parameters, call_args, namespace:) + code_generator.define_cached_method(name, as: mangled_name, namespace: namespace) do |batch| + body = if CALL_COMPILABLE_REGEXP.match?(target_name) + "self.#{target_name}(#{call_args.join(", ")})" else - call_args.unshift(":'#{proxy_target}'") + call_args.unshift(":'#{target_name}'") "send(#{call_args.join(", ")})" end - modifier = parameters == FORWARD_PARAMETERS ? "ruby2_keywords " : "" - batch << - "#{modifier}def #{mangled_name}(#{parameters || ''})" << + "def #{mangled_name}(#{parameters || ''})" << body << "end" end end @@ -473,11 +446,11 @@ AttributeMethod = Struct.new(:proxy_target, :attr_name) def initialize(prefix: "", suffix: "", parameters: nil) @prefix = prefix @suffix = suffix - @parameters = parameters.nil? ? FORWARD_PARAMETERS : parameters + @parameters = parameters.nil? ? "..." : parameters @regex = /\A(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})\z/ @proxy_target = "#{@prefix}attribute#{@suffix}" @method_name = "#{prefix}%s#{suffix}" end @@ -501,27 +474,25 @@ # <tt>ActiveRecord::Base#attributes=</tt>. # # It's also possible to instantiate related objects, so a <tt>Client</tt> # class belonging to the +clients+ table with a +master_id+ foreign key # can instantiate master through <tt>Client#master</tt>. - def method_missing(method, *args, &block) + def method_missing(method, ...) if respond_to_without_attributes?(method, true) super else - match = matched_attribute_method(method.to_s) - match ? attribute_missing(match, *args, &block) : super + match = matched_attribute_method(method.name) + match ? attribute_missing(match, ...) : super end end - 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. - def attribute_missing(match, *args, &block) - __send__(match.proxy_target, match.attr_name, *args, &block) + def attribute_missing(match, ...) + __send__(match.proxy_target, match.attr_name, ...) end - ruby2_keywords(:attribute_missing) # A +Person+ instance with a +name+ attribute can ask # <tt>person.respond_to?(:name)</tt>, <tt>person.respond_to?(:name=)</tt>, # and <tt>person.respond_to?(:name?)</tt> which will all return +true+. alias :respond_to_without_attributes? :respond_to?