Skip to Content
Course content

170: Practice Exercise: Building a Custom Validation DSL

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

Think of a custom DSL (Domain Specific Language) as a "Cheat Sheet" you leave for your future self. Imagine you're running a professional kitchen. Instead of telling your sous-chef, "Every time a burger order comes in, check if it has a bun, then check if the meat is cooked, then check if there's lettuce," you just pin a checklist to the wall: Burgers: [Bun, Cooked, Lettuce]. Now, the sous-chef doesn't need a manual of instructions; they just look at the checklist and verify the plate against it.

In Ruby, we do the exact same thing when we build a validation DSL. Instead of writing a massive validate method filled with if/else blocks for every single attribute, we create a way to declare the rules at the top of the class. We're essentially building that "checklist" during the class definition phase, and then executing it later when an object is actually instantiated.

Turning Declarations into Data

The secret to a DSL is that the method you call (like validates) isn't actually performing the validation right then. It's just recording a rule. I usually use a class-level array to store these rules. When you call validates :email, presence: true, you're not checking the email; you're just pushing a hash like { attribute: :email, rule: :presence } into a list.

module Validatable
  def self.included(base)
    base.extend(ClassMethods)
  end

  module ClassMethods
    def validations
      @validations ||= []
    end

    def validates(attribute, options = {})
      validations << { attribute: attribute, options: options }
    end
  end

  def valid?
    self.class.validations.all? do |v|
      check_validation(v[:attribute], v[:options])
    end
  end

  private

  def check_validation(attr, options)
    value = send(attr)
    options.each do |rule, requirement|
      return false if rule == :presence && (value.nil? || value.to_s.empty?)
      return false if rule == :min_length && value.to_s.length < requirement
    end
    true
  end
end

Why This Beats Standard Methods

You might be thinking, "Why not just write a method?" Because this approach separates what should be validated from how it's validated. If I decide tomorrow that "presence" should also exclude strings that are just whitespace, I only have to change the code in one place—the check_validation method—and every single class using this DSL is automatically updated.

I've seen too many projects where developers copy-paste the same if name.nil? || name.empty? block across ten different models. It's a maintenance nightmare. By using a DSL, your model stays clean and readable. It reads like a configuration file rather than a script.

Connecting the Dots

Notice how the valid? instance method acts as the bridge. It reaches back up to the class level (the "checklist" on the wall) and iterates through those rules using the current object's data. We use send(attr) to dynamically call the getter method for whichever attribute was declared in the DSL. This is the magic that allows one module to work for a User class, a Product class, or a BlogPost class without knowing anything about their specific attributes beforehand.




📋 Practical Task

Exercise: Building a Flexible User Profile Validator

Your task is to expand the Validatable module to handle more complex rules. You need to implement a system that can handle both simple presence checks and custom range checks.

Requirements:

  • Implement the Validatable module as shown in the lesson.
  • Add a new validation rule called :range. This rule should take a Range object (e.g., 18..99) and ensure the attribute's value falls within that range.
  • Create a User class that includes Validatable.
  • In the User class, use the DSL to:
    • Ensure username is present.
    • Ensure age is within the range of 13..120.
  • Test your implementation with three scenarios:
    1. A user with a username and age 25 (should be valid).
    2. A user with no username (should be invalid).
    3. A user with an age of 10 (should be invalid).

Bonus: Try making the check_validation method return a list of error messages instead of a simple boolean.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.