Skip to Content
Course content

244: Building a Simple Attribute Validation Macro

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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_reader for the attribute.
  • Define a setter method that checks if the incoming value is an instance of expected_class.
  • Raise a TypeError with 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.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.