Share
Demystifying some of the magic behind Rails
Rails is renowned for its elegance, productivity, and “magic” that simplifies web development. As a technical writer for the official Ruby on Rails guides, I’ve had the opportunity to dive into the “magic” of Rails to enhance my understanding of the source code and provide clear explanations in the guides.
In this talk, I’ll share my journey of inspecting modules, exploring concepts, and sharing insights gained from my experience to help attendees better understand how some Rails components work internally. Whether you are a beginner eager to understand the framework’s foundations or an experienced developer looking to deepen your knowledge, this dive into the source code will enhance your proficiency and demystify the framework’s inner workings.
Ridhwana Khan
Transcript
90 slides
-
👩💻Lead Software Engineer, 📝 Technical Writer, & 👥 Community Builder
….but most importantly, A Cat Mom. -
2. ✍ Writing & Team Review 1. 💻 Audit 3.
🔎 PR & Community Review Three Step Process: -
What Can You Expect? 1. Navigating the Source Code: Explore
strategies to trace through the Rails -
What Can You Expect? 1. Navigating the Source Code: Explore
strategies to trace through the Rails 2. Examples: Tracing through three different modules in Rails -
What Can You Expect? 1. Navigating the Source Code: Explore
strategies to trace through the Rails 2. Examples: Tracing through three different modules in Rails 3. Metaprogramming Evaluation: Benefits & Drawbacks of metaprogramming within the Rails source -
What Can You Expect? 1. Navigating the Source Code: Explore
strategies to trace through the Rails 2. Examples: Tracing through three different modules in Rails 3. Metaprogramming Evaluation: Benefits & Drawbacks of metaprogramming within the Rails source 4. Using these Patterns in the Wild: Apply these techniques to different use cases -
Patterns & Conventions 236 results - 109 files actioncable/lib/action_cable/connection/tagged_logger_proxy.rb: 35
%i( debug info warn error fatal unknown ).each do |severity| 36: define_method(severity) do |message = nil, &block| 37 log severity, message, &block actionpack/lib/abstract_controller/callbacks.rb: 230 [:before, :after, :around].each do |callback| 231: define_method "#{callback}_action" do |*names, &blk| 232 _insert_callbacks(names, blk) do |name, options| 236 237: define_method "prepend_#{callback}_action" do |*names, &blk| 238 _insert_callbacks(names, blk) do |name, options| 244 # the allowed parameters. 245: define_method "skip_#{callback}_action" do |*names| 246 _insert_callbacks(names) do |name, options| actionpack/lib/abstract_controller/railties/routes_helpers.rb: 11 Module.new do 12: define_method(:inherited) do |klass| 13 super(klass) actionpack/lib/action_controller/metal/flash.rb: 37 38: define_method(type) do 39 request.flash[type] actionpack/lib/action_controller/metal/renderers.rb: 75 def self.add(key, &block) 76: define_method(_render_with_renderer_method_name(key), &block) 77 RENDERERS << key.to_sym actionpack/lib/action_dispatch/http/content_security_policy.rb: 187 DIRECTIVES.each do |name, directive| 188: define_method(name) do |*sources| 189 if sources.first actionpack/lib/action_dispatch/http/permissions_policy.rb: 122 DIRECTIVES.each do |name, directive| 123: define_method(name) do |*sources| 124 if sources.first actionpack/lib/action_dispatch/routing/mapper.rb: 710 711: define_method :find_script_name do |options| 712 if options.key?(:script_name) && options[:script_name].present? actionpack/lib/action_dispatch/routing/route_set.rb: 335 def define_url_helper(mod, name, helper, url_strategy) 336: mod.define_method(name) do |*args| 337 last = args.last 517 MountedHelpers.class_eval do 518: define_method "_#{name}" do 519 RoutesProxy.new(routes, _routes_context, helpers, script_namer) 616 # extra conveniences for working with @_routes. 617: define_method(:_routes) { @_routes || routes } -
Testing Suite class Pirate < ActiveRecord::Base belongs_to :parrot, validate: true
has_one :ship has_many :birds end class TestAutosaveAssociationValidationMethodsGeneration < ActiveRecord::TestCase def setup super @pirate = Pirate.new end test "should generate validation methods for has_many associations" do assert_respond_to @pirate, :validate_associated_records_for_birds end test "should generate validation methods for has_one associations with :validate => true" do assert_respond_to @pirate, :validate_associated_records_for_ship end test "should generate validation methods for belongs_to associations with :validate => true" do assert_respond_to @pirate, :validate_associated_records_for_parrot end end -
Testing Suite class Pirate < ActiveRecord::Base belongs_to :parrot, validate: true
has_one :ship has_many :birds end class TestAutosaveAssociationValidationMethodsGeneration < ActiveRecord::TestCase def setup super @pirate = Pirate.new end test "should generate validation methods for has_many associations" do assert_respond_to @pirate, :validate_associated_records_for_birds end test "should generate validation methods for has_one associations with :validate => true" do assert_respond_to @pirate, :validate_associated_records_for_ship end test "should generate validation methods for belongs_to associations with :validate => true" do assert_respond_to @pirate, :validate_associated_records_for_parrot end end -
What is Metaprogramming? Metaprogramming allows a program to write or
modify its own code. -
What does this module do? class ArticlesController < ApplicationController def
show @article = Article.find(params[:id]) respond_to do |format| format.html # renders show.html.erb format.json { render json: @article } end end end GET /articles/1.json Accept: application/json -
Example Code module AbstractController module Collector def self.generate_method_for_mime(mime) sym =
mime.is_a?(Symbol) ? mime : mime.to_sym class_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{sym}(...) custom(Mime[:#{sym}], ...) end RUBY end Mime::SET.each do |mime| generate_method_for_mime(mime) end Mime::Type.register_callback do |mime| generate_method_for_mime(mime) unless instance_methods.include?(mime.to_sym) end private def method_missing(symbol, ...) unless mime_constant = Mime[symbol] raise NoMethodError, "To respond to a custom format, register it as a MIME type first: " \ "https://guides.rubyonrails.org/action_controller_overview.html#restful-downloads. " \ "If you meant to respond to a variant like :tablet or :phone, not a custom format, " \ "be sure to nest your variant response within a format response: " \ "format.html { |html| html.tablet { ... } }" end if Mime::SET.include?(mime_constant) AbstractController::Collector.generate_method_for_mime(mime_constant) public_send(symbol, ...) else super end end end end generate_method_for_mime method_missing -
module AbstractController module Collector def self.generate_method_for_mime(mime) ... end ... private
def method_missing(symbol, ...) unless mime_constant = Mime[symbol] raise NoMethodError, "To respond to a custom format, ..." end if Mime::SET.include?(mime_constant) AbstractController::Collector.generate_method_for_mime(mime_constant) public_send(symbol, ...) else super end end end end GET /articles/1.json Accept: application/json -
module AbstractController module Collector def self.generate_method_for_mime(mime) ... end ... private
def method_missing(symbol, ...) unless mime_constant = Mime[symbol] raise NoMethodError, "To respond to a custom format, ..." end if Mime::SET.include?(mime_constant) AbstractController::Collector.generate_method_for_mime(mime_constant) public_send(symbol, ...) else super end end end end GET /articles/1.json Accept: application/json def json ... end -
module AbstractController module Collector def self.generate_method_for_mime(mime) ... end ... private
def method_missing(symbol, ...) unless mime_constant = Mime[symbol] raise NoMethodError, "To respond to a custom format, ..." end if Mime::SET.include?(mime_constant) AbstractController::Collector.generate_method_for_mime(mime_constant) public_send(symbol, ...) else super end end end end :json GET /articles/1.json Accept: application/json -
module AbstractController module Collector def self.generate_method_for_mime(mime) ... end ... private
def method_missing(symbol, ...) unless mime_constant = Mime[symbol] raise NoMethodError, "To respond to a custom format, ..." end if Mime::SET.include?(mime_constant) AbstractController::Collector.generate_method_for_mime(mime_constant) public_send(symbol, ...) else super end end end end :json GET /articles/1.json Accept: application/json -
module AbstractController module Collector def self.generate_method_for_mime(mime) sym = mime.is_a?(Symbol) ?
mime : mime.to_sym class_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{sym}(...) custom(Mime[:#{sym}], ...) end RUBY end ... private def method_missing(symbol, ...) ... AbstractController::Collector.generate_method_for_mime(mime_constant) public_send(symbol, ...) ... end end end GET /articles/1.json Accept: application/json -
module AbstractController module Collector def self.generate_method_for_mime(mime) sym = mime.is_a?(Symbol) ?
mime : mime.to_sym class_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{sym}(...) custom(Mime[:#{sym}], ...) end RUBY end ... private def method_missing(symbol, ...) ... AbstractController::Collector.generate_method_for_mime(mime_constant) public_send(symbol, ...) ... end end end GET /articles/1.json Accept: application/json -
module AbstractController module Collector def self.generate_method_for_mime(mime) sym = mime.is_a?(Symbol) ?
mime : mime.to_sym class_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{sym}(...) custom(Mime[:#{sym}], ...) end RUBY end ... private def method_missing(symbol, ...) ... AbstractController::Collector.generate_method_for_mime(mime_constant) public_send(symbol, ...) ... end end end def :json(…) custom(Mime[:json], ...) end GET /articles/1.json Accept: application/json :json -
module AbstractController module Collector def self.generate_method_for_mime(mime) sym = mime.is_a?(Symbol) ?
mime : mime.to_sym class_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{sym}(...) custom(Mime[:#{sym}], ...) end RUBY end ... private def method_missing(symbol, ...) ... AbstractController::Collector.generate_method_for_mime(mime_constant) public_send(symbol, ...) ... end end end GET /articles/1.json Accept: application/json -
module AbstractController module Collector def self.generate_method_for_mime(mime) sym = mime.is_a?(Symbol) ?
mime : mime.to_sym class_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{sym}(...) custom(Mime[:#{sym}], ...) end RUBY end ... private def method_missing(symbol, ...) ... AbstractController::Collector.generate_method_for_mime(mime_constant) public_send(symbol, ...) ... end end end GET /articles/1.json Accept: application/json :json def json(...) custom(Mime[:json], ...) end def json ... end -
module AbstractController module Collector def self.generate_method_for_mime(mime) sym = mime.is_a?(Symbol) ?
mime : mime.to_sym class_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{sym}(...) custom(Mime[:#{sym}], ...) end RUBY end ... private def method_missing(symbol, ...) ... AbstractController::Collector.generate_method_for_mime(mime_constant) public_send(symbol, ...) ... end end end GET /articles/1.json Accept: application/json def json(...) custom(Mime[:json], ...) end { Mime[:json] => Proc.new { render json: @article } } :json def json ... end -
What does this module do? class ArticlesController < ApplicationController def
show @article = Article.find(params[:id]) respond_to do |format| format.html # renders show.html.erb format.json { render json: @article } end end end GET /articles/1.json Accept: application/json { Mime[:json] => Proc.new { render json: @article } } -
Examples class Post < ActiveRecord::Base belongs_to :author end post =
Post.find(7) author = Author.find(19) post.author # similar to Author.find(post.author_id) post.author = author # similar to post.author_id = author.id post.build_author # similar to post.author = Author.new post.create_author # similar to post.author = Author.new; post.author.save; post.author post.create_author! # similar to post.author = Author.new; post.author.save!; post.author post.reload_author post.reset_author post.author_changed? post.author_previously_changed? -
Examples class Post < ActiveRecord::Base belongs_to :author end post =
Post.find(7) author = Author.find(19) post.author # similar to Author.find(post.author_id) post.author = author # similar to post.author_id = author.id post.build_author # similar to post.author = Author.new post.create_author # similar to post.author = Author.new; post.author.save; post.author post.create_author! # similar to post.author = Author.new; post.author.save!; post.author post.reload_author post.reset_author post.author_changed? post.author_previously_changed? -
module ActiveRecord::Associations::Builder # :nodoc: class Association # :nodoc: ... def
self.build(model, name, scope, options, &block) if model.dangerous_attribute_method?(name) raise ArgumentError, "You tried to define an association named #{name} on the model " \ "#{model.name}, but this will conflict with a method #{name} " \ "already defined by Active Record. Please choose a different " \ "association name." end reflection = create_reflection(model, name, scope, options, &block) define_accessors(model, reflection) define_callbacks(model, reflection) define_validations(model, reflection) define_change_tracking_methods(model, reflection) reflection end ... end end -
But first, what is a Reflection? class Post < ApplicationRecord
belongs_to :author has_many :comments end A re fl ection in Rails represents the metadata about an association between two Active Record models. It contains details such as the type of association, the associated class, & options like foreign key constraints. -
But first, what is a Reflection? class Post < ApplicationRecord
belongs_to :author has_many :comments end A re fl ection in Rails represents the metadata about an association between two Active Record models. It contains details such as the type of association, the associated class, & options like foreign key constraints. reflection = Post.reflect_on_association(:author) puts reflection.macro #=> :belongs_to puts reflection.class_name #=> "Author" -
module ActiveRecord::Associations::Builder # :nodoc: class Association # :nodoc: ... def
self.build(model, name, scope, options, &block) if model.dangerous_attribute_method?(name) raise ArgumentError, "You tried to define an association named #{name} on the model " \ "#{model.name}, but this will conflict with a method #{name} " \ "already defined by Active Record. Please choose a different " \ "association name." end reflection = create_reflection(model, name, scope, options, &block) define_accessors(model, reflection) define_callbacks(model, reflection) define_validations(model, reflection) define_change_tracking_methods(model, reflection) reflection end ... end end -
module ActiveRecord::Associations::Builder # :nodoc: class Association # :nodoc: ... def
self.build(model, name, scope, options, &block) ... reflection = create_reflection(model, name, scope, options, &block) define_accessors(model, reflection) define_callbacks(model, reflection) define_validations(model, reflection) define_change_tracking_methods(model, reflection) reflection end ... end end define_accessors post.author post.author = author class Post < ApplicationRecord belongs_to :author end -
module ActiveRecord::Associations::Builder # :nodoc: class Association # :nodoc: # Defines
the setter & getter methods for the association # class Post < ActiveRecord::Base # has_many :comments # end # # Post.first.comments & Post.first.comments= methods are defined by this method... def self.define_accessors(model, reflection) mixin = model.generated_association_methods name = reflection.name define_readers(mixin, name) define_writers(mixin, name) end end end class Post < ApplicationRecord belongs_to :author end post.author post.author = author Post::GeneratedAssociationMethods define_accessors -
module ActiveRecord::Associations::Builder # :nodoc: class Association # :nodoc: # Defines
the setter & getter methods for the association # class Post < ActiveRecord::Base # has_many :comments # end # # Post.first.comments & Post.first.comments= methods are defined by this method... def self.define_accessors(model, reflection) mixin = model.generated_association_methods name = reflection.name define_readers(mixin, name) define_writers(mixin, name) end end end define_readers(Post::GeneratedAssociationMethods, "author") define_writers(Post::GeneratedAssociationMethods, "author") define_accessors class Post < ApplicationRecord belongs_to :author end post.author post.author = author Post::GeneratedAssociationMethods -
module ActiveRecord::Associations::Builder class Association def self.define_accessors(model, reflection) mixin = model.generated_association_methods
name = reflection.name define_readers(mixin, name) define_writers(mixin, name) end def self.define_readers(mixin, name) mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 def #{name} association(:#{name}).reader end CODE end def self.define_writers(mixin, name) mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 def #{name}=(value) association(:#{name}).writer(value) end CODE end end end post.author post.author = author -
module ActiveRecord::Associations::Builder class Association ... def self.define_readers(mixin, name) mixin.class_eval <<-CODE,
__FILE__, __LINE__ + 1 def #{name} association(:#{name}).reader end CODE end def self.define_writers(mixin, name) mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 def #{name}=(value) association(:#{name}).writer(value) end CODE end end end def author association(:author).reader end post.author post.author = author Post::GeneratedAssociationMethods "author" -
module ActiveRecord::Associations::Builder class Association ... def self.define_readers(mixin, name) mixin.class_eval <<-CODE,
__FILE__, __LINE__ + 1 def #{name} association(:#{name}).reader end CODE end def self.define_writers(mixin, name) mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 def #{name}=(value) association(:#{name}).writer(value) end CODE end end end def author= author association(:author).writer(author) end "author" post.author post.author = author Post::GeneratedAssociationMethods -
module ActiveRecord::Associations::Builder class Association ... def self.define_readers(mixin, name) mixin.class_eval <<-CODE,
__FILE__, __LINE__ + 1 def #{name} association(:#{name}).reader end CODE end def self.define_writers(mixin, name) mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 def #{name}=(value) association(:#{name}).writer(value) end CODE end end end def author= author association(:author).writer(author) end post.author post.author = author def author association(:author).reader end -
# rails/activerecord/lib/active_record/associations/builder/belongs_to.rb module ActiveRecord::Associations::Builder # :nodoc: class BelongsTo < SingularAssociation
# :nodoc: ... def self.define_change_tracking_methods(model, reflection) model.generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1 def #{reflection.name}_changed? association(:#{reflection.name}).target_changed? end def #{reflection.name}_previously_changed? association(:#{reflection.name}).target_previously_changed? end CODE end ... end end Define Change Tracking Methods post.author_changed? post.author_previously_changed? def author_changed? association(:author).target_changed? end def author_previously_changed? association(:author).target_previously_changed? end -
# rails/activerecord/lib/active_record/associations/builder/singular_association.rb module ActiveRecord::Associations::Builder # :nodoc: class SingularAssociation < Association
# :nodoc: ... def self.define_accessors(model, reflection) ... mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 def reload_#{name} association(:#{name}).force_reload_reader end def reset_#{name} association(:#{name}).reset end CODE end ... end end post.reload_author post.reset_author def reload_author association(:author).force_reload_reader end def reset_author association(:author).reset end Define Reload & Reset Methods -
# rails/activerecord/lib/active_record/associations/builder/singular_association.rb module ActiveRecord::Associations::Builder # :nodoc: class SingularAssociation < Association
# :nodoc: ... # Defines the (build|create)_association methods for # belongs_to or has_one association def self.define_constructors(mixin, name) mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 def build_#{name}(*args, &block) association(:#{name}).build(*args, &block) end def create_#{name}(*args, &block) association(:#{name}).create(*args, &block) end def create_#{name}!(*args, &block) association(:#{name}).create!(*args, &block) end CODE end ... end end post.build_author post.create_author post.create_author! def build_author(*args, &block) association(:author).build(*args, &block) end def create_author(*args, &block) association(:author).create(*args, &block) end Define Constructors def create_author!(*args, &block) association(:author).create(*args, &block) end -
module ActiveRecord module AutosaveAssociation module AssociationBuilderExtension # :nodoc: def self.build(model,
reflection) ... end ... end module ClassMethods # :nodoc: private ... def define_autosave_validation_callbacks(reflection) ... end ... end end end end class Post < ApplicationRecord belongs_to :author end -
module ActiveRecord module AutosaveAssociation module ClassMethods # :nodoc: private def
define_autosave_validation_callbacks(reflection) validation_method = :"validate_associated_records_for_#{reflection.name}" if reflection.validate? && !method_defined?(validation_method) if reflection.collection? method = :validate_collection_association elsif reflection.has_one? method = :validate_has_one_association else method = :validate_belongs_to_association end define_non_cyclic_method(validation_method) { send(method, reflection) } validate validation_method after_validation :_ensure_no_duplicate_errors end end ... end end end end class Post < ApplicationRecord belongs_to :author end <ActiveRecord::Reflection::BelongsToReflection:0x00007fffcac51b60> -
module ActiveRecord module AutosaveAssociation module ClassMethods # :nodoc: private def
define_autosave_validation_callbacks(reflection) validation_method = :"validate_associated_records_for_#{reflection.name}" if reflection.validate? && !method_defined?(validation_method) if reflection.collection? method = :validate_collection_association elsif reflection.has_one? method = :validate_has_one_association else method = :validate_belongs_to_association end define_non_cyclic_method(validation_method) { send(method, reflection) } validate validation_method after_validation :_ensure_no_duplicate_errors end end ... end end end end class Post < ApplicationRecord belongs_to :author end -
module ActiveRecord module AutosaveAssociation module ClassMethods # :nodoc: private def
define_autosave_validation_callbacks(reflection) validation_method = :"validate_associated_records_for_#{reflection.name}" if reflection.validate? && !method_defined?(validation_method) if reflection.collection? method = :validate_collection_association elsif reflection.has_one? method = :validate_has_one_association else method = :validate_belongs_to_association end define_non_cyclic_method(validation_method) { send(method, reflection) } validate validation_method after_validation :_ensure_no_duplicate_errors end end ... end end end end class Post < ApplicationRecord belongs_to :author end :validate_associated_ records_for_author -
module ActiveRecord module AutosaveAssociation module ClassMethods # :nodoc: private def
define_autosave_validation_callbacks(reflection) validation_method = :"validate_associated_records_for_#{reflection.name}" if reflection.validate? && !method_defined?(validation_method) if reflection.collection? method = :validate_collection_association elsif reflection.has_one? method = :validate_has_one_association else method = :validate_belongs_to_association end define_non_cyclic_method(validation_method) { send(method, reflection) } validate validation_method after_validation :_ensure_no_duplicate_errors end end ... end end end end send(:validate_belongs_to_association, <ActiveRecord::Reflection::BelongsToRef lection:0x00007fffcac51b60>) class Post < ApplicationRecord belongs_to :author end :validate_associated_ records_for_author -
module ActiveRecord module AutosaveAssociation module ClassMethods # :nodoc: private def
define_autosave_validation_callbacks(reflection) validation_method = :"validate_associated_records_for_#{reflection.name}" if reflection.validate? && !method_defined?(validation_method) if reflection.collection? method = :validate_collection_association elsif reflection.has_one? method = :validate_has_one_association else method = :validate_belongs_to_association end define_non_cyclic_method(validation_method) { send(method, reflection) } validate validation_method after_validation :_ensure_no_duplicate_errors end end ... end end end end send(:validate_belongs_to_association, <ActiveRecord::Reflection::BelongsToRef lection:0x00007fffcac51b60>) class Post < ApplicationRecord belongs_to :author end :validate_associated_ records_for_author -
module ActiveRecord module AutosaveAssociation module ClassMethods # :nodoc: private def
define_non_cyclic_method(name, &block) return if method_defined?(name, false) define_method(name) do |*args| result = true; @_already_called ||= {} # Loop prevention for validation of associations unless @_already_called[name] begin @_already_called[name] = true result = instance_eval(&block) ensure @_already_called[name] = false end end result end end ... end end end end class Post < ApplicationRecord belongs_to :author end -
module ActiveRecord module AutosaveAssociation module ClassMethods # :nodoc: private def
define_non_cyclic_method(name, &block) return if method_defined?(name, false) define_method(name) do |*args| result = true; @_already_called ||= {} # Loop prevention for validation of associations unless @_already_called[name] begin @_already_called[name] = true result = instance_eval(&block) ensure @_already_called[name] = false end end result end end ... end end end end :validate_associated_ records_for_author class Post < ApplicationRecord belongs_to :author end -
module ActiveRecord module AutosaveAssociation module ClassMethods # :nodoc: private def
define_non_cyclic_method(name, &block) return if method_defined?(name, false) define_method(name) do |*args| result = true; @_already_called ||= {} # Loop prevention for validation of associations unless @_already_called[name] begin @_already_called[name] = true result = instance_eval(&block) ensure @_already_called[name] = false end end result end end ... end end end end define_method(:validate_associated_records_for_author) class Post < ApplicationRecord belongs_to :author end -
module ActiveRecord module AutosaveAssociation module ClassMethods # :nodoc: private def
define_non_cyclic_method(name, &block) return if method_defined?(name, false) define_method(name) do |*args| result = true; @_already_called ||= {} # Loop prevention for validation of associations unless @_already_called[name] begin @_already_called[name] = true result = instance_eval(&block) ensure @_already_called[name] = false end end result end end ... end end end end class Post < ApplicationRecord belongs_to :author end -
module ActiveRecord module AutosaveAssociation module ClassMethods # :nodoc: private def
define_non_cyclic_method(name, &block) return if method_defined?(name, false) define_method(name) do |*args| result = true; @_already_called ||= {} # Loop prevention for validation of associations unless @_already_called[name] begin @_already_called[name] = true result = instance_eval(&block) ensure @_already_called[name] = false end end result end end ... end end end end class Post < ApplicationRecord belongs_to :author end -
module ActiveRecord module AutosaveAssociation module ClassMethods # :nodoc: private def
define_non_cyclic_method(name, &block) return if method_defined?(name, false) define_method(name) do |*args| result = true; @_already_called ||= {} # Loop prevention for validation of associations unless @_already_called[name] begin @_already_called[name] = true result = instance_eval(&block) ensure @_already_called[name] = false end end result end end ... end end end end result = instance_eval { send(:validate_belongs_to_association, <ActiveRecord::Reflection::BelongsToReflection:0x00007f ffcac51b60>) } class Post < ApplicationRecord belongs_to :author end -
module ActiveRecord module AutosaveAssociation module ClassMethods # :nodoc: private def
define_autosave_validation_callbacks(reflection) validation_method = :"validate_associated_records_for_#{reflection.name}" if reflection.validate? && !method_defined?(validation_method) if reflection.collection? method = :validate_collection_association elsif reflection.has_one? method = :validate_has_one_association else method = :validate_belongs_to_association end define_non_cyclic_method(validation_method) { send(method, reflection) } validate validation_method after_validation :_ensure_no_duplicate_errors end end ... end end end end validate :validate_associated_records_for_author class Post < ApplicationRecord belongs_to :author end -
module ActiveRecord module AutosaveAssociation module ClassMethods # :nodoc: private def
define_autosave_validation_callbacks(reflection) validation_method = :"validate_associated_records_for_#{reflection.name}" if reflection.validate? && !method_defined?(validation_method) if reflection.collection? method = :validate_collection_association elsif reflection.has_one? method = :validate_has_one_association else method = :validate_belongs_to_association end define_non_cyclic_method(validation_method) { send(method, reflection) } validate validation_method after_validation :_ensure_no_duplicate_errors end end ... end end end end validate :validate_associated_records_for_author class Post < ApplicationRecord belongs_to :author end -
Convention over Configuration DRY Principle Dynamic Behaviour Simplifying API Design
Benefits of Metaprogramming in Rails -
Convention over Configuration DRY Principle Dynamic Behaviour Simplifying API Design
Benefits of Metaprogramming in Rails -
Convention over Configuration DRY Principle Dynamic Behaviour Simplifying API Design
Benefits of Metaprogramming in Rails -
Convention over Configuration DRY Principle Dynamic Behaviour Simplifying API Design
Benefits of Metaprogramming in Rails -
class Profile < ApplicationRecord belongs_to :user validates :user_id, uniqueness: true
validates :location, :website_url, length: { maximum: 100 } validates :website_url, url: { allow_blank: true, no_local: true, schemes: %w[https http] } validates_with ProfileValidator ATTRIBUTE_NAME_REGEX = /(?<attribute_name>\w+)=?/ CACHE_KEY = "profile/attributes".freeze # Static fields are columns on the profiles table; they have no relationship # to a ProfileField record. These are columns we can safely assume exist for # any profile on a given Forem. STATIC_FIELDS = %w[summary location website_url].freeze # Update the Rails cache with the currently available attributes. def self.refresh_attributes! Rails.cache.delete(CACHE_KEY) attributes end def self.attributes Rails.cache.fetch(CACHE_KEY, expires_in: 24.hours) do ProfileField.pluck(:attribute_name) end end def self.static_fields STATIC_FIELDS end def clear! update(data: {}) end # Lazily add accessors for profile fields on first use def method_missing(method_name, *args, **kwargs, &block) match = method_name.match(ATTRIBUTE_NAME_REGEX) super unless match field = ProfileField.find_by(attribute_name: match[:attribute_name]) super unless field self.class.instance_eval do store_accessor :data, field.attribute_name.to_sym end public_send(method_name, *args, **kwargs, &block) end # Defining this is not only a good practice in general, it's also necessary # for `update` to work since the `_assign_attribute` helper it uses performs # an explicit `responds_to?` check. def respond_to_missing?(method_name, include_private = false) match = method_name.match(ATTRIBUTE_NAME_REGEX) return true if match && match[:attribute_name].in?(self.class.attributes) super end end https://github.com/forem/forem/ blob/main/app/models/profile.rb