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
244: Building a Simple Attribute Validation Macro
I've been working on a small internal tool for managing a team's project assignments, and I noticed a pattern in my Project class that was starting to annoy me. I have a handful of attributes—like title, deadline, and lead_developer—and for every single one of them, I'm writing the exact same boilerplate to ensure the value isn't nil before assigning it.
class Project
attr_accessor :title, :deadline
def title=(val)
raise ArgumentError, "Title cannot be blank" if val.nil? || val.strip.empty?
@title = val
end
def deadline=(val)
raise ArgumentError, "Deadline cannot be blank" if val.nil?
@deadline = val
end
end
Tired of the same old setters
It works, sure. But it's tedious. If I add ten more fields, I'm just copying and pasting the same if statement over and over. In Ruby, when we see a repetitive pattern like this inside a class definition, it's a sign that we should probably be building a "macro."
If you've used Rails, you've seen this with things like has_many or validates :presence. These aren't just methods; they are class methods that dynamically generate other methods for us. Let's try to build a simple version of that here. I want to be able to just write validates_presence_of :title and have Ruby do the heavy lifting.
Moving logic into a class method
First, I'll try creating a class method. Since I'm calling this directly inside the class body, I need to define it on the singleton class of Project. I'll use self.validates_presence_of.
class Project
def self.validates_presence_of(attribute_name)
puts "Validating #{attribute_name}!"
end
validates_presence_of :title
end
Running this just prints "Validating title!". That's a start, but it doesn't actually do anything to the title attribute. I need this method to actually create the setter method (title=) for me. This is where define_method comes in. It allows us to create an instance method programmatically.
Generating methods on the fly
Let's try to hook define_method into our macro. I'll use the attribute_name to name the method. In Ruby, setters end with an equals sign, so I'll have to interpolate that into the method name.
class Project
def self.validates_presence_of(attr_name)
# We define a method named "title=" (for example)
define_method("#{attr_name}=") do |val|
raise ArgumentError, "#{attr_name} cannot be blank" if val.nil?
instance_variable_set("@#{attr_name}", val)
end
end
validates_presence_of :title
validates_presence_of :deadline
end
p = Project.new
p.title = "Build the Macro" # Works!
p.deadline = nil # Raises ArgumentError: deadline cannot be blank
This is much better. I've replaced those bulky setter methods with a single line of configuration. But I noticed a problem: I've lost the ability to read the attributes. I used to have attr_accessor, but if I use attr_accessor :title after my macro, the accessor's setter will overwrite the one my macro just created. If I put it before, it works, but it's a bit redundant to define the setter twice.
Polishing the macro logic
Since my macro is already handling the setter, I only really need the getter. I'll swap attr_accessor for attr_reader to keep things clean. I also want to make this more robust; currently, an empty string "" would pass the nil? check, which probably isn't what I want for a title.
Here is the refined version. I'll add a check for empty strings and ensure I'm only creating the reader if it doesn't already exist.
class Project
def self.validates_presence_of(attr_name)
# Create the reader so we can actually access the value
attr_reader attr_name
# Create the custom setter
define_method("#{attr_name}=") do |val|
if val.nil? || (val.respond_to?(:empty?) && val.empty?)
raise ArgumentError, "#{attr_name} cannot be blank"
end
instance_variable_set("@#{attr_name}", val)
end
end
validates_presence_of :title
validates_presence_of :deadline
end
Now the class is incredibly lean. The logic for "presence validation" is encapsulated in the macro, and adding new validated attributes is a one-liner. We've essentially built a tiny DSL (Domain Specific Language) inside our Project class. It’s a powerful pattern, but a word of caution: overusing define_method can make debugging harder because the methods don't exist as physical lines of code in your file—they are birthed at runtime.
📋 Practical Task
Exercise: Building a Type-Validation Macro
In this exercise, you will extend the concept of the validation macro. Instead of just checking for presence, you need to create a macro that ensures an attribute is of a specific Ruby class.
Create a class called Invoice. Implement a class method called validates_type_of(attr_name, expected_class). This macro should:
- Create an
attr_readerfor the attribute. - Define a setter method that checks if the incoming value is an instance of
expected_class. - Raise a
TypeErrorwith a descriptive message (e.g., "Amount must be a Numeric") if the validation fails. - Assign the value to the instance variable if the validation passes.
Test your implementation by adding the following attributes to your Invoice class:
validates_type_of :amount, Numeric
validates_type_of :customer_name, String
Verify that assigning a string to amount or a number to customer_name triggers the TypeError.
There are no comments for now.