Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Batch attribute methods definition #39094

Merged
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
85 changes: 60 additions & 25 deletions activemodel/lib/active_model/attribute_methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -207,10 +207,12 @@ def attribute_method_affix(*affixes)
# 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

Expand Down Expand Up @@ -249,7 +251,9 @@ def attribute_alias(name)
# 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
Expand Down Expand Up @@ -281,21 +285,23 @@ def define_attribute_methods(*attr_names)
# 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)

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
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}"

if respond_to?(generate_method, true)
send(generate_method, attr_name.to_s)
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.
Expand Down Expand Up @@ -329,6 +335,37 @@ def undefine_attribute_methods
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"]
end

def <<(source_line)
@sources << source_line
end

def execute
@owner.module_eval(@sources.join(";"), @path, @line - 1)
end
end
private_constant :CodeGenerator

def generated_attribute_methods
@generated_attribute_methods ||= Module.new.tap { |mod| include mod }
end
Expand Down Expand Up @@ -359,7 +396,7 @@ def attribute_method_matchers_matching(method_name)
# 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
Expand All @@ -374,13 +411,11 @@ def define_proxy_call(include_private, mod, name, target, *extra)
"send(:'#{target}', #{extra})"
end

mod.module_eval <<-RUBY, __FILE__, __LINE__ + 1
# frozen_string_literal: true
#{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:
Expand Down