-
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
170: Practice Exercise: Building a Custom Validation DSL
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
Validatablemodule 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
Userclass that includesValidatable. - In the
Userclass, use the DSL to:- Ensure
usernameis present. - Ensure
ageis within the range of13..120.
- Ensure
- Test your implementation with three scenarios:
- A user with a username and age 25 (should be valid).
- A user with no username (should be invalid).
- 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.
There are no comments for now.