ruby
48 lines · 2 tabs
Maya Patel
Jan 2026
2 tabs
class SlugValidator < ActiveModel::Validator
SLUG_REGEX = /\A[a-z0-9]+(?:-[a-z0-9]+)*\z/
def validate(record)
slug = record.send(options[:attribute] || :slug)
if slug.blank?
record.errors.add(:slug, 'cannot be blank')
return
end
unless slug.match?(SLUG_REGEX)
record.errors.add(:slug, 'must contain only lowercase letters, numbers, and hyphens')
end
if slug.length < options[:minimum] || slug.length > options[:maximum]
record.errors.add(:slug, "must be between #{options[:minimum]} and #{options[:maximum]} characters")
end
# Check for reserved words
reserved_slugs = %w[admin api new edit delete]
if reserved_slugs.include?(slug)
record.errors.add(:slug, 'is reserved and cannot be used')
end
end
end
class Post < ApplicationRecord
validates :title, presence: true, length: { minimum: 5, maximum: 100 }
validates :slug, uniqueness: { scope: :author_id }
validates_with SlugValidator, attribute: :slug, minimum: 3, maximum: 60
# Custom validation method
validate :published_at_in_past, if: :published?
before_validation :generate_slug, if: :title_changed?
private
def published_at_in_past
if published_at.present? && published_at > Time.current
errors.add(:published_at, 'cannot be in the future')
end
end
def generate_slug
self.slug = title.parameterize
end
end
2 files · ruby
Explain with highlit
Custom validators encapsulate complex validation rules that go beyond built-in validators. I create validator classes for business logic like email format verification, slug uniqueness, or credit card validation. Custom validators inherit from ActiveModel::Validator and implement a validate method that adds errors to the record. They're reusable across models and testable in isolation. For simple one-off validations, I use validate with a method name. Complex conditional validations use if/unless options. I keep validators focused—each validates one concern. Error messages use I18n for internationalization. This pattern keeps models clean while enforcing business rules consistently.
Related snips
ruby
class CommentsController < ApplicationController
before_action :set_post
def create
@comment = @post.comments.build(comment_params)
System test: asserting Turbo Stream responses
rails
hotwire
turbo
by codesnips
4 tabs
ruby
class Post < ApplicationRecord
belongs_to :author, class_name: 'User'
has_many :comments, dependent: :destroy
scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
scope :draft, -> { where(published_at: nil) }
ActiveRecord scopes for reusable query logic
rails
activerecord
patterns
by Alex Kumar
1 tab
ruby
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
rails
caching
http-caching
by Alex Kumar
1 tab
ruby
class PostsController < ApplicationController
def index
@posts = Post.includes(:author)
.order(created_at: :desc)
.page(params[:page])
.per(10)
Turbo Frames: infinite scroll with lazy-loading frame
rails
turbo
hotwire
by codesnips
4 tabs
ruby
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
rails
performance
streaming
by codesnips
3 tabs
erb
<form data-controller="query-sync" data-action="change->query-sync#apply">
<select name="status" class="rounded border p-2">
<option value="">Any</option>
<option value="open">Open</option>
<option value="closed">Closed</option>
</select>
Filter UI that syncs query params via Stimulus (no front-end router)
rails
hotwire
stimulus
by Henry Kim
2 tabs
Share this code
Here's the card — post it anywhere.