Slide 1

Slide 1 text

Presentation Title Author Demystifying some of the magic behind Rails @Ridhwana_K

Slide 2

Slide 2 text

Cape Town

Slide 3

Slide 3 text

πŸ‘©πŸ’»Lead Software Engineer, πŸ“ Technical Writer, & πŸ‘₯ Community Builder ….but most importantly, A Cat Mom.

Slide 4

Slide 4 text

Zeus Zoey

Slide 5

Slide 5 text

Technical Writer for Rails Guides

Slide 6

Slide 6 text

Rails Documentation

Slide 7

Slide 7 text

Rails Documentation Team

Slide 8

Slide 8 text

2. ✍ Writing & Team Review 1. πŸ’» Audit 3. πŸ”Ž PR & Community Review Three Step Process:

Slide 9

Slide 9 text

What Can You Expect? 1. Navigating the Source Code: Explore strategies to trace through the Rails

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

Navigating the Source Code

Slide 14

Slide 14 text

Rails API docs & Ruby on Rails Guides https://guides.rubyonrails.org/

Slide 15

Slide 15 text

Rails API docs & https://api.rubyonrails.org/ Ruby on Rails API

Slide 16

Slide 16 text

Rails API docs & https://github.com/rails/rails/blob/main/activemodel/README.rdoc README Files

Slide 17

Slide 17 text

source_location User.method(:find_by).source_location ["/Users/username/.rbenv/versions/ 3.1.4/lib/ruby/gems/3.1.0/gems/ activerecord-6.1.7.3/lib/active_record/ relation/finder_methods.rb", 271]

Slide 18

Slide 18 text

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 }

Slide 19

Slide 19 text

Code Editor & Debugger EDITOR="code" bundle open activerecord EDITOR="code" bundle open βž•

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

Github Copilot

Slide 23

Slide 23 text

What is Metaprogramming? Metaprogramming allows a program to write or modify its own code.

Slide 24

Slide 24 text

Examples of Rails Patterns

Slide 25

Slide 25 text

Example 1: MIME types in a Controller Action AbstractController::Collector actionpack/lib/abstract_controller/collector.rb

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

method_missing class_eval public_send

Slide 40

Slide 40 text

Example 2: Building Associations ActiveRecord::Associations::Builder::Association active_record/lib/active_record/associations/builder/association.rb

Slide 41

Slide 41 text

https://api.rubyonrails.org/classes/ActiveRecord/Associations/ ClassMethods.html#method-i-belongs_to Methods Added By belongs_to

Slide 42

Slide 42 text

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?

Slide 43

Slide 43 text

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?

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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.

Slide 46

Slide 46 text

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"

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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"

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

# 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

Slide 56

Slide 56 text

# 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

Slide 57

Slide 57 text

# 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

Slide 58

Slide 58 text

Example 3: Defining Autosave Validation Callbacks ActiveRecord::AutosaveAssociation active_record/lib/active_record/autosave_association.rb

Slide 59

Slide 59 text

Autosave Validations class Post < ApplicationRecord belongs_to :author end

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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, ) class Post < ApplicationRecord belongs_to :author end :validate_associated_ records_for_author

Slide 65

Slide 65 text

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, ) class Post < ApplicationRecord belongs_to :author end :validate_associated_ records_for_author

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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, ) } class Post < ApplicationRecord belongs_to :author end

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

Benefits & Drawbacks of Metaprogramming in Rails

Slide 75

Slide 75 text

Convention over Configuration DRY Principle Dynamic Behaviour Simplifying API Design Benefits of Metaprogramming in Rails

Slide 76

Slide 76 text

Convention over Configuration DRY Principle Dynamic Behaviour Simplifying API Design Benefits of Metaprogramming in Rails

Slide 77

Slide 77 text

Convention over Configuration DRY Principle Dynamic Behaviour Simplifying API Design Benefits of Metaprogramming in Rails

Slide 78

Slide 78 text

Convention over Configuration DRY Principle Dynamic Behaviour Simplifying API Design Benefits of Metaprogramming in Rails

Slide 79

Slide 79 text

Drawbacks of Metaprogramming in Rails Performance Overheads Increased Abstraction Maintainability Issues

Slide 80

Slide 80 text

Drawbacks of Metaprogramming in Rails Performance Overheads Increased Abstraction Maintainability Issues

Slide 81

Slide 81 text

Drawbacks of Metaprogramming in Rails Performance Overheads Increased Abstraction Maintainability Issues

Slide 82

Slide 82 text

Our developer experience

Slide 83

Slide 83 text

Using these Patterns in the Wild

Slide 84

Slide 84 text

Dynamic Profile Fields UserProfile.favourite_plant = "cactus"

Slide 85

Slide 85 text

Dynamic Profile Fields UserProfile.favourite_plant = "cactus" UserProfile.available_for_hire = true

Slide 86

Slide 86 text

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 = /(?\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

Slide 87

Slide 87 text

Empowering your Rails Journey Explore, Contribute, & Grow

Slide 88

Slide 88 text

Empowering your Rails Journey Explore, Contribute, & Grow

Slide 89

Slide 89 text

Empowering your Rails Journey Explore, Contribute, & Grow

Slide 90

Slide 90 text

Thank you! πŸ’› @Ridhwana_K