Ruby
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Methods and Blocks
-
Section 4: Object-Oriented Ruby
-
Section 5: Metaprogramming
-
Section 6: Working with Data and Files
-
Section 7: Ruby Frameworks Overview
-
Section 8: Ecosystem and Testing
-
Section 9: Practical Projects
-
Section 10: Interview Practice
-
Section 11: Data Structures and Algorithms in Ruby
-
Section 12: More Practice Exercises
-
Section 13: Enumerable and Functional Style
-
Section 14: More OOP Practice
-
Section 15: More Testing
-
Section 16: Enumerable and Comparable Modules In Depth
-
Section 17: Ruby Standard Library: Core Utilities
-
Section 18: Ruby Standard Library: Data and Security
-
Section 19: Ruby Standard Library: CLI and Text
-
Section 20: Ruby Networking
-
Section 21: Ruby on Rails Deep Dive
-
Section 22: Ruby Metaprogramming Deep Dive
-
Section 23: Ruby Design Patterns
-
Section 24: Ruby Concurrency
-
Section 25: Ruby Testing Deep Dive
-
Section 26: Ruby Gems and Packaging
-
Section 27: Ruby Performance
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Rails API Development
-
Section 31: Rails Authentication and Authorization
-
Section 32: Rails Testing Deep Dive
-
Section 33: Rails Performance
-
Section 34: Rails Deployment
-
Section 35: Sinatra and Lightweight Ruby Web Apps
-
Section 36: More Ruby Language Deep Dive
-
Section 37: Ruby 3.x Modern Features
-
Section 38: More Data Structures in Ruby
-
Section 39: More Practical Projects
-
Section 40: Ruby Ecosystem Tools
-
Section 41: More Practice and Review
-
Section 42: Final Practice and Mastery
-
Section 43: Ruby Interview Deep Dive
-
Section 44: Ruby Background Processing Deep Dive
-
Section 45: Ruby GraphQL
-
Section 46: Ruby Object Model Deep Dive
-
Section 47: Ruby Hanami Framework Overview
-
Section 48: Ruby gRPC and Protocol Buffers
-
Section 49: Ruby Data Processing
-
Section 50: Ruby Search Integration
-
Section 51: Ruby File Upload and Media
-
Section 52: Ruby Email and Notifications
-
Section 53: Ruby Admin Panels
-
Section 54: Ruby Feature Flags and Experimentation
-
Section 55: Ruby Monitoring and Observability
-
Section 56: Ruby Docker and Deployment Deep Dive
-
Section 57: Ruby Security Deep Dive
-
Section 58: More Advanced Metaprogramming
-
Section 59: More Final Projects
240: Building a Simple ORM-Like DSL from Scratch
Imagine you're filling out a custom order form at a high-end sandwich shop. You don't walk into the kitchen and tell the chef, "First, take a slice of sourdough, then apply 2 grams of Dijon mustard, then place three slices of heirloom tomato on top." That would be tedious and prone to error. Instead, you just check a few boxes: [x] Sourdough, [x] Dijon, [x] Tomato. You've provided a high-level description of what you want, and the chef—who knows the "internal logic" of the kitchen—handles the actual implementation of building that sandwich.
A Domain Specific Language (DSL) in Ruby works exactly like that order form. When we use an ORM (Object-Relational Mapper) like ActiveRecord, we aren't writing the SQL to create tables or the Ruby code to map columns to variables manually. We're just "checking boxes" using a DSL, and the ORM handles the heavy lifting behind the scenes.
Turning Class Declarations into Data
The secret to building a DSL is realizing that class-level methods can be used to store metadata. When you call a method inside a class definition but outside any instance method, you're executing code the moment the class is loaded. We can use this to "register" what our model should look like.
Let's start by building a BaseModel. I want my subclasses to be able to say column :name, :string, and have that information remembered by the class.
class BaseModel
def self.column(name, type)
# We store the column definitions in a class-level hash
@columns ||= {}
@columns[name] = type
puts "Registered column #{name} as #{type}"
end
def self.columns
@columns || {}
end
end
class User < BaseModel
column :username, :string
column :email, :string
column :age, :integer
end
If you run this, you'll see the "Registered column" messages pop up immediately. We haven't even created a User.new yet, but the User class now "knows" its own schema. This is the "order form" part of our analogy.
The Magic of Dynamic Method Definition
Knowing the columns is great, but it's useless if we still have to write attr_accessor :username for every single column. That defeats the purpose of the DSL. I want the column method to actually build the getter and setter methods for me while the class is being defined.
This is where define_method comes in. It allows us to create methods programmatically. I've always found this part of Ruby a bit mind-bending at first, but once it clicks, you realize you can generate hundreds of methods in a few lines of code.
class BaseModel
def self.column(name, type)
@columns ||= {}
@columns[name] = type
# Define the getter
define_method(name) do
instance_variable_get("@#{name}")
end
# Define the setter
define_method("#{name}=") do |value|
# Here is where we'd normally add type validation
instance_variable_set("@#{name}", value)
end
end
def self.columns
@columns || {}
end
end
# Now, let's see it in action
class Product < BaseModel
column :title, :string
column :price, :float
end
laptop = Product.new
laptop.title = "MacBook Pro"
laptop.price = 2499.99
puts "#{laptop.title} costs #{laptop.price}"
# Output: MacBook Pro costs 2499.99
Bridging the Gap to a Mock Database
To make this feel like a real ORM, we need a way to "save" this data. Since we aren't plugging in a real PostgreSQL or MySQL database for this lesson, we'll simulate one using a global hash. The goal is to map our object's instance variables back to the "columns" we defined in our DSL.
Notice how I'm using the self.class.columns call inside the save method. This allows the instance to look back at the "blueprint" we created at the class level to determine what data actually belongs in the database.
$mock_db = {}
class BaseModel
# ... (column and define_method logic from above) ...
def save
# Use the class-level metadata to build a data hash
data = {}
self.class.columns.each_key do |col_name|
data[col_name] = send(col_name)
end
# Simulate saving to a DB using the object's memory address as an ID
id = self.object_id
$mock_db[id] = data
puts "Saved to DB: {$mock_db[id]}"
id
end
end
By combining class-level state, define_method, and instance_variable_set, we've essentially built a miniature version of the engine that powers Rails. You're no longer writing boilerplate; you're writing a language that describes your data, and letting Ruby handle the implementation.
📋 Practical Task
Implement a Type-Checking Validator for the Mini-ORM
Right now, our BaseModel allows you to set any value to any column, regardless of the type we specified in the column :name, :type call. For example, you could set a :integer column to a String, and the ORM wouldn't care.
Your Task: Modify the column method in the BaseModel class so that the dynamically generated setter method validates the input type before assigning the value.
- If the column type is
:integer, the setter should raise anArgumentErrorif the value provided is not anInteger. - If the column type is
:string, the setter should raise anArgumentErrorif the value provided is not aString. - If the column type is
:float, the setter should raise anArgumentErrorif the value provided is not aFloat.
Test your implementation by creating a User model with an :age column of type :integer and attempting to assign a string to it. The code should crash with the appropriate error message.
There are no comments for now.