Ruby 3.3+ development specialist covering Rails 7.2, ActiveRecord, Hotwire/Turbo, and modern Ruby patterns. Use when developing Ruby APIs, web applications, or Rails projects.
Installation
Details
Usage
After installing, this skill will be available to your AI coding assistant.
Verify installation:
npx agent-skills-cli listSkill Instructions
name: moai-lang-ruby description: Ruby 3.3+ development specialist covering Rails 7.2, ActiveRecord, Hotwire/Turbo, and modern Ruby patterns. Use when developing Ruby APIs, web applications, or Rails projects. version: 1.0.0 updated: 2025-12-07 status: active allowed-tools: Read, Grep, Glob, Bash, mcp__context7__resolve-library-id, mcp__context7__get-library-docs
Quick Reference (30 seconds)
Ruby 3.3+ Development Specialist - Rails 7.2, ActiveRecord, Hotwire/Turbo, RSpec, and modern Ruby patterns.
Auto-Triggers: .rb files, Gemfile, Rakefile, config.ru, Rails/Ruby discussions
Core Capabilities:
- Ruby 3.3 Features: YJIT production-ready, pattern matching, Data class, endless methods
- Web Framework: Rails 7.2 with Turbo, Stimulus, ActiveRecord
- Frontend: Hotwire (Turbo + Stimulus) for SPA-like experiences
- Testing: RSpec with factories, request specs, system specs
- Background Jobs: Sidekiq with ActiveJob
- Package Management: Bundler with Gemfile
- Code Quality: RuboCop with Rails cops
- Database: ActiveRecord with migrations, associations, scopes
Quick Patterns
Rails Controller:
class UsersController < ApplicationController
before_action :set_user, only: %i[show edit update destroy]
def index
@users = User.all
end
def create
@user = User.new(user_params)
respond_to do |format|
if @user.save
format.html { redirect_to @user, notice: "User was successfully created." }
format.turbo_stream
else
format.html { render :new, status: :unprocessable_entity }
end
end
end
private
def set_user
@user = User.find(params[:id])
end
def user_params
params.require(:user).permit(:name, :email)
end
end
ActiveRecord Model:
class User < ApplicationRecord
has_many :posts, dependent: :destroy
has_one :profile, dependent: :destroy
validates :email, presence: true, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :name, presence: true, length: { minimum: 2, maximum: 100 }
scope :active, -> { where(active: true) }
scope :recent, -> { order(created_at: :desc) }
def full_name
"#{first_name} #{last_name}".strip
end
end
RSpec Test:
RSpec.describe User, type: :model do
describe "validations" do
it { is_expected.to validate_presence_of(:email) }
it { is_expected.to validate_uniqueness_of(:email) }
end
describe "#full_name" do
let(:user) { build(:user, first_name: "John", last_name: "Doe") }
it "returns the full name" do
expect(user.full_name).to eq("John Doe")
end
end
end
Implementation Guide (5 minutes)
Ruby 3.3 New Features
YJIT (Production-Ready):
- Enabled by default in Ruby 3.3
- 15-20% performance improvement for Rails apps
- Enable:
ruby --yjitorRUBY_YJIT_ENABLE=1 - Check status:
RubyVM::YJIT.enabled? - Memory optimization with better code caching
Pattern Matching (case/in):
def process_response(response)
case response
in { status: "ok", data: data }
puts "Success: #{data}"
in { status: "error", message: msg }
puts "Error: #{msg}"
in { status: status } if %w[pending processing].include?(status)
puts "In progress..."
else
puts "Unknown response"
end
end
# Array pattern matching
case [1, 2, 3]
in [first, *rest]
puts "First: #{first}, Rest: #{rest}"
end
# Hash pattern matching with guard
case { name: "John", age: 25 }
in { name:, age: } if age >= 18
puts "Adult: #{name}"
end
Data Class (Immutable Structs):
# Define immutable data class
User = Data.define(:name, :email) do
def greeting
"Hello, #{name}!"
end
end
user = User.new(name: "John", email: "john@example.com")
user.name # => "John"
user.greeting # => "Hello, John!"
# user.name = "Jane" # => FrozenError
# With default values
Point = Data.define(:x, :y) do
def self.origin
new(x: 0, y: 0)
end
end
Endless Method Definition:
class Calculator
# Single expression methods
def add(a, b) = a + b
def multiply(a, b) = a * b
# With method chaining
def double(n) = n * 2
def square(n) = n ** 2
# Predicate methods
def positive?(n) = n > 0
def even?(n) = n.even?
end
Rails 7.2 Patterns
Application Setup:
# Gemfile
source "https://rubygems.org"
gem "rails", "~> 7.2.0"
gem "pg", "~> 1.5"
gem "puma", ">= 6.0"
gem "turbo-rails"
gem "stimulus-rails"
gem "jbuilder"
gem "redis", ">= 5.0"
gem "sidekiq", "~> 7.0"
group :development, :test do
gem "rspec-rails", "~> 7.0"
gem "factory_bot_rails"
gem "faker"
gem "rubocop-rails", require: false
end
group :test do
gem "capybara"
gem "shoulda-matchers"
end
Model with Concerns:
# app/models/concerns/sluggable.rb
module Sluggable
extend ActiveSupport::Concern
included do
before_validation :generate_slug, on: :create
validates :slug, presence: true, uniqueness: true
end
def to_param
slug
end
private
def generate_slug
self.slug = title.parameterize if title.present? && slug.blank?
end
end
# app/models/post.rb
class Post < ApplicationRecord
include Sluggable
belongs_to :user
has_many :comments, dependent: :destroy
has_many_attached :images
validates :title, presence: true, length: { minimum: 5 }
validates :content, presence: true
scope :published, -> { where(published: true) }
scope :by_user, ->(user) { where(user: user) }
end
Service Objects:
# app/services/user_registration_service.rb
class UserRegistrationService
def initialize(user_params)
@user_params = user_params
end
def call
user = User.new(@user_params)
ActiveRecord::Base.transaction do
user.save!
create_profile(user)
send_welcome_email(user)
end
Result.new(success: true, user: user)
rescue ActiveRecord::RecordInvalid => e
Result.new(success: false, errors: e.record.errors)
end
private
def create_profile(user)
user.create_profile!(bio: "New user")
end
def send_welcome_email(user)
UserMailer.welcome(user).deliver_later
end
Result = Data.define(:success, :user, :errors) do
def success? = success
def failure? = !success
end
end
Hotwire (Turbo + Stimulus)
Turbo Frames:
<!-- app/views/posts/index.html.erb -->
<%= turbo_frame_tag "posts" do %>
<% @posts.each do |post| %>
<%= render post %>
<% end %>
<% end %>
<!-- app/views/posts/_post.html.erb -->
<%= turbo_frame_tag dom_id(post) do %>
<article class="post">
<h2><%= link_to post.title, post %></h2>
<p><%= truncate(post.content, length: 200) %></p>
<%= link_to "Edit", edit_post_path(post) %>
</article>
<% end %>
Turbo Streams:
# app/controllers/posts_controller.rb
def create
@post = current_user.posts.build(post_params)
respond_to do |format|
if @post.save
format.turbo_stream
format.html { redirect_to @post }
else
format.html { render :new, status: :unprocessable_entity }
end
end
end
# app/views/posts/create.turbo_stream.erb
<%= turbo_stream.prepend "posts", @post %>
<%= turbo_stream.update "new_post", partial: "posts/form", locals: { post: Post.new } %>
Stimulus Controller:
// app/javascript/controllers/form_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["input", "submit", "output"]
static values = { url: String }
connect() {
this.validate()
}
validate() {
const isValid = this.inputTargets.every(input => input.value.length > 0)
this.submitTarget.disabled = !isValid
}
async submit(event) {
event.preventDefault()
const response = await fetch(this.urlValue, {
method: "POST",
body: new FormData(this.element),
headers: { "Accept": "text/vnd.turbo-stream.html" }
})
if (response.ok) {
this.element.reset()
}
}
}
RSpec Testing Patterns
Model Specs:
# spec/models/user_spec.rb
RSpec.describe User, type: :model do
describe "associations" do
it { is_expected.to have_many(:posts).dependent(:destroy) }
it { is_expected.to have_one(:profile).dependent(:destroy) }
end
describe "validations" do
subject { build(:user) }
it { is_expected.to validate_presence_of(:email) }
it { is_expected.to validate_uniqueness_of(:email).case_insensitive }
it { is_expected.to validate_length_of(:name).is_at_least(2).is_at_most(100) }
end
describe "scopes" do
describe ".active" do
let!(:active_user) { create(:user, active: true) }
let!(:inactive_user) { create(:user, active: false) }
it "returns only active users" do
expect(described_class.active).to contain_exactly(active_user)
end
end
end
describe "#full_name" do
context "when both names are present" do
let(:user) { build(:user, first_name: "John", last_name: "Doe") }
it "returns the full name" do
expect(user.full_name).to eq("John Doe")
end
end
context "when last name is missing" do
let(:user) { build(:user, first_name: "John", last_name: nil) }
it "returns only first name" do
expect(user.full_name).to eq("John")
end
end
end
end
Request Specs:
# spec/requests/posts_spec.rb
RSpec.describe "Posts", type: :request do
let(:user) { create(:user) }
before { sign_in user }
describe "GET /posts" do
let!(:posts) { create_list(:post, 3, user: user) }
it "returns a successful response" do
get posts_path
expect(response).to have_http_status(:ok)
end
it "displays all posts" do
get posts_path
posts.each do |post|
expect(response.body).to include(post.title)
end
end
end
describe "POST /posts" do
let(:valid_params) { { post: attributes_for(:post) } }
let(:invalid_params) { { post: { title: "" } } }
context "with valid parameters" do
it "creates a new post" do
expect {
post posts_path, params: valid_params
}.to change(Post, :count).by(1)
end
it "redirects to the created post" do
post posts_path, params: valid_params
expect(response).to redirect_to(Post.last)
end
end
context "with invalid parameters" do
it "does not create a new post" do
expect {
post posts_path, params: invalid_params
}.not_to change(Post, :count)
end
it "returns unprocessable entity status" do
post posts_path, params: invalid_params
expect(response).to have_http_status(:unprocessable_entity)
end
end
end
end
Factory Bot Patterns:
# spec/factories/users.rb
FactoryBot.define do
factory :user do
sequence(:email) { |n| "user#{n}@example.com" }
name { Faker::Name.name }
password { "password123" }
active { true }
trait :inactive do
active { false }
end
trait :admin do
role { :admin }
end
trait :with_posts do
transient do
posts_count { 3 }
end
after(:create) do |user, evaluator|
create_list(:post, evaluator.posts_count, user: user)
end
end
factory :admin_user, traits: [:admin]
end
end
Sidekiq Background Jobs
Job Definition:
# app/jobs/process_order_job.rb
class ProcessOrderJob < ApplicationJob
queue_as :default
retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3
discard_on ActiveJob::DeserializationError
def perform(order_id)
order = Order.find(order_id)
ActiveRecord::Base.transaction do
order.process!
order.update!(processed_at: Time.current)
OrderMailer.confirmation(order).deliver_later
end
end
end
# Sidekiq configuration
# config/initializers/sidekiq.rb
Sidekiq.configure_server do |config|
config.redis = { url: ENV.fetch("REDIS_URL", "redis://localhost:6379/1") }
end
Sidekiq.configure_client do |config|
config.redis = { url: ENV.fetch("REDIS_URL", "redis://localhost:6379/1") }
end
ActiveRecord Advanced Patterns
Scopes and Query Objects:
class Post < ApplicationRecord
scope :published, -> { where(published: true) }
scope :recent, -> { order(created_at: :desc) }
scope :by_author, ->(author) { where(author: author) }
scope :search, ->(query) { where("title ILIKE ?", "%#{query}%") }
# Complex scope with joins
scope :with_comments, -> {
joins(:comments).group(:id).having("COUNT(comments.id) > 0")
}
# Scope returning specific columns
scope :titles_only, -> { select(:id, :title) }
end
# Query Object
class PostSearchQuery
def initialize(relation = Post.all)
@relation = relation
end
def call(params)
@relation
.then { |r| filter_by_status(r, params[:status]) }
.then { |r| filter_by_date(r, params[:start_date], params[:end_date]) }
.then { |r| search_by_title(r, params[:query]) }
.then { |r| paginate(r, params[:page], params[:per_page]) }
end
private
def filter_by_status(relation, status)
return relation if status.blank?
relation.where(status: status)
end
def filter_by_date(relation, start_date, end_date)
relation = relation.where("created_at >= ?", start_date) if start_date
relation = relation.where("created_at <= ?", end_date) if end_date
relation
end
def search_by_title(relation, query)
return relation if query.blank?
relation.where("title ILIKE ?", "%#{query}%")
end
def paginate(relation, page, per_page)
page ||= 1
per_page ||= 25
relation.limit(per_page).offset((page.to_i - 1) * per_page.to_i)
end
end
Advanced Implementation (10+ minutes)
For comprehensive coverage including:
- Production deployment patterns (Docker, Kubernetes)
- Advanced ActiveRecord patterns (polymorphic, STI)
- Action Cable real-time features
- Performance optimization techniques
- Security best practices
- CI/CD integration patterns
See:
- reference.md - Complete reference documentation
- examples.md - Production-ready code examples
Context7 Library Mappings
/rails/rails - Ruby on Rails web framework
/rspec/rspec - RSpec testing framework
/hotwired/turbo-rails - Turbo for Rails
/hotwired/stimulus-rails - Stimulus for Rails
/sidekiq/sidekiq - Background job processing
/rubocop/rubocop - Ruby style guide enforcement
/thoughtbot/factory_bot - Test data factories
Works Well With
moai-domain-backend- REST API and web application architecturemoai-domain-database- SQL patterns and ActiveRecord optimizationmoai-quality-testing- TDD and testing strategiesmoai-essentials-debug- AI-powered debuggingmoai-foundation-trust- TRUST 5 quality principles
Troubleshooting
Common Issues:
Ruby Version Check:
ruby --version # Should be 3.3+
ruby -e "puts RubyVM::YJIT.enabled?" # Check YJIT status
Rails Version Check:
rails --version # Should be 7.2+
bundle exec rails about # Full environment info
Database Connection Issues:
- Check
config/database.ymlconfiguration - Ensure PostgreSQL/MySQL service is running
- Run
rails db:createif database doesn't exist
Asset Pipeline Issues:
# Precompile assets
rails assets:precompile
# Clear asset cache
rails assets:clobber
RSpec Setup Issues:
# Install RSpec
rails generate rspec:install
# Run specific test
bundle exec rspec spec/models/user_spec.rb
# Run with verbose output
bundle exec rspec --format documentation
Turbo/Stimulus Issues:
# Rebuild JavaScript
rails javascript:install:esbuild
# Clear Turbo cache
rails turbo:install
Last Updated: 2025-12-07 Status: Active (v1.0.0)
More by junseokandylee
View allJava 21 LTS development specialist covering Spring Boot 3.3, virtual threads, pattern matching, and enterprise patterns. Use when building enterprise applications, microservices, or Spring projects.
Clerk modern authentication specialist covering WebAuthn, passkeys, passwordless, and beautiful UI components. Use when implementing modern auth with great UX.
SPEC workflow orchestration with EARS format, requirement clarification, and Plan-Run-Sync integration for MoAI-ADK development methodology
Enterprise context and session management with token budget optimization and state persistence
