moai-lang-ruby
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.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install junseokandylee-claudeautomate-moai-lang-ruby
Repository
Skill path: .claude/skills/moai-lang-ruby
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.
Open repositoryBest for
Primary workflow: Ship Full Stack.
Technical facets: Full Stack.
Target audience: everyone.
License: Unknown.
Original source
Catalog source: SkillHub Club.
Repository owner: junseokandylee.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install moai-lang-ruby into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/junseokandylee/ClaudeAutomate before adding moai-lang-ruby to shared team environments
- Use moai-lang-ruby for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
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:
```ruby
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:
```ruby
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:
```ruby
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 --yjit` or `RUBY_YJIT_ENABLE=1`
- Check status: `RubyVM::YJIT.enabled?`
Pattern Matching (case/in):
```ruby
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
```
Data Class (Immutable Structs):
```ruby
User = Data.define(:name, :email) do
def greeting
"Hello, #{name}!"
end
end
user = User.new(name: "John", email: "[email protected]")
user.name # => "John"
user.greeting # => "Hello, John!"
```
Endless Method Definition:
```ruby
class Calculator
def add(a, b) = a + b
def multiply(a, b) = a * b
def positive?(n) = n > 0
end
```
### Rails 7.2 Patterns
Application Setup:
```ruby
# 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 "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:
```ruby
# 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) }
end
```
Service Objects:
```ruby
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:
```erb
<!-- 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>
</article>
<% end %>
```
Turbo Streams:
```ruby
# 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:
```javascript
// app/javascript/controllers/form_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["input", "submit"]
connect() {
this.validate()
}
validate() {
const isValid = this.inputTargets.every(input => input.value.length > 0)
this.submitTarget.disabled = !isValid
}
}
```
### RSpec Testing Basics
Factory Bot Patterns:
```ruby
# spec/factories/users.rb
FactoryBot.define do
factory :user do
sequence(:email) { |n| "user#{n}@example.com" }
name { Faker::Name.name }
password { "password123" }
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
end
end
```
---
## Advanced Implementation (10+ minutes)
For comprehensive coverage including:
- Production deployment patterns (Docker, Kubernetes)
- Advanced ActiveRecord patterns (polymorphic, STI, query objects)
- Action Cable real-time features
- Performance optimization techniques
- Security best practices
- CI/CD integration patterns
- Complete RSpec testing patterns
See:
- [Advanced Patterns](modules/advanced-patterns.md) - Production patterns and advanced features
- [Testing Patterns](modules/testing-patterns.md) - Complete RSpec testing guide
---
## 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 architecture
- `moai-domain-database` - SQL patterns and ActiveRecord optimization
- `moai-workflow-testing` - TDD and testing strategies
- `moai-essentials-debug` - AI-powered debugging
- `moai-foundation-quality` - TRUST 5 quality principles
---
## Troubleshooting
Common Issues:
Ruby Version Check:
```bash
ruby --version # Should be 3.3+
ruby -e "puts RubyVM::YJIT.enabled?" # Check YJIT status
```
Rails Version Check:
```bash
rails --version # Should be 7.2+
bundle exec rails about # Full environment info
```
Database Connection Issues:
- Check `config/database.yml` configuration
- Ensure PostgreSQL/MySQL service is running
- Run `rails db:create` if database doesn't exist
Asset Pipeline Issues:
```bash
rails assets:precompile
rails assets:clobber
```
RSpec Setup Issues:
```bash
rails generate rspec:install
bundle exec rspec spec/models/user_spec.rb
bundle exec rspec --format documentation
```
Turbo/Stimulus Issues:
```bash
rails javascript:install:esbuild
rails turbo:install
```
---
Last Updated: 2025-12-07
Status: Active (v1.0.0)
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### modules/advanced-patterns.md
```markdown
# Ruby Advanced Patterns
## Sidekiq Background Jobs
Job Definition:
```ruby
# 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:
```ruby
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
```
## Production Deployment Patterns
Docker Configuration:
```dockerfile
# Dockerfile
FROM ruby:3.3-slim
RUN apt-get update -qq && apt-get install -y \
build-essential \
libpq-dev \
nodejs \
npm \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY Gemfile Gemfile.lock ./
RUN bundle config set --local deployment 'true' && \
bundle config set --local without 'development test' && \
bundle install
COPY . .
RUN bundle exec rails assets:precompile
ENV RAILS_ENV=production
ENV RAILS_LOG_TO_STDOUT=true
EXPOSE 3000
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]
```
Puma Configuration:
```ruby
# config/puma.rb
workers ENV.fetch("WEB_CONCURRENCY") { 2 }
threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
threads threads_count, threads_count
preload_app!
port ENV.fetch("PORT") { 3000 }
environment ENV.fetch("RAILS_ENV") { "development" }
plugin :tmp_restart
on_worker_boot do
ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
end
```
## Advanced ActiveRecord Patterns
Polymorphic Associations:
```ruby
class Comment < ApplicationRecord
belongs_to :commentable, polymorphic: true
belongs_to :user
end
class Post < ApplicationRecord
has_many :comments, as: :commentable, dependent: :destroy
end
class Image < ApplicationRecord
has_many :comments, as: :commentable, dependent: :destroy
end
```
Single Table Inheritance (STI):
```ruby
class Vehicle < ApplicationRecord
validates :brand, presence: true
end
class Car < Vehicle
validates :doors, numericality: { greater_than: 0 }
end
class Motorcycle < Vehicle
validates :engine_cc, numericality: { greater_than: 0 }
end
```
## Action Cable Real-Time Features
Channel Implementation:
```ruby
# app/channels/notification_channel.rb
class NotificationChannel < ApplicationCable::Channel
def subscribed
stream_from "notifications:#{current_user.id}"
end
def unsubscribed
stop_all_streams
end
end
# Broadcasting notifications
class NotificationService
def self.notify(user, message)
ActionCable.server.broadcast(
"notifications:#{user.id}",
{ message: message, timestamp: Time.current.iso8601 }
)
end
end
```
## Performance Optimization
N+1 Query Prevention:
```ruby
# Bad: N+1 queries
User.all.each do |user|
puts user.posts.count # Executes a query for each user
end
# Good: Eager loading
User.includes(:posts).each do |user|
puts user.posts.size # Uses preloaded data
end
# Counter cache for counts
class Post < ApplicationRecord
belongs_to :user, counter_cache: true
end
# Migration for counter cache
add_column :users, :posts_count, :integer, default: 0
```
Database Indexing:
```ruby
# migration
class AddIndexesToPosts < ActiveRecord::Migration[7.2]
def change
add_index :posts, :user_id
add_index :posts, :status
add_index :posts, [:status, :published_at]
add_index :posts, :title, using: :gin, opclass: :gin_trgm_ops
end
end
```
## Security Best Practices
Strong Parameters:
```ruby
class UsersController < ApplicationController
private
def user_params
params.require(:user).permit(:name, :email, :password, :password_confirmation)
end
end
```
CSRF Protection:
```ruby
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
end
```
SQL Injection Prevention:
```ruby
# Bad: Direct interpolation
User.where("email = '#{params[:email]}'")
# Good: Parameterized query
User.where(email: params[:email])
User.where("email = ?", params[:email])
```
## CI/CD Integration
GitHub Actions Workflow:
```yaml
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'
bundler-cache: true
- name: Setup database
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/test
run: |
bundle exec rails db:create db:migrate
- name: Run tests
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/test
run: bundle exec rspec
```
```
### modules/testing-patterns.md
```markdown
# Ruby Testing Patterns
## RSpec Model Specs
Complete Model Testing:
```ruby
# 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
```
## RSpec Request Specs
API Testing:
```ruby
# 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
Advanced Factories:
```ruby
# 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
# spec/factories/posts.rb
FactoryBot.define do
factory :post do
sequence(:title) { |n| "Post Title #{n}" }
content { Faker::Lorem.paragraphs(number: 3).join("\n\n") }
user
published { false }
trait :published do
published { true }
published_at { Time.current }
end
trait :with_comments do
transient do
comments_count { 5 }
end
after(:create) do |post, evaluator|
create_list(:comment, evaluator.comments_count, post: post)
end
end
end
end
```
## System Specs with Capybara
End-to-End Testing:
```ruby
# spec/system/user_registration_spec.rb
RSpec.describe "User Registration", type: :system do
before do
driven_by(:selenium_chrome_headless)
end
it "allows a user to register" do
visit new_user_registration_path
fill_in "Email", with: "[email protected]"
fill_in "Password", with: "password123"
fill_in "Password confirmation", with: "password123"
click_button "Sign up"
expect(page).to have_content("Welcome! You have signed up successfully.")
expect(User.find_by(email: "[email protected]")).to be_present
end
it "shows validation errors for invalid input" do
visit new_user_registration_path
fill_in "Email", with: "invalid-email"
click_button "Sign up"
expect(page).to have_content("Email is invalid")
end
end
```
## Service Object Testing
Testing Service Layer:
```ruby
# spec/services/user_registration_service_spec.rb
RSpec.describe UserRegistrationService do
describe "#call" do
let(:valid_params) do
{
name: "John Doe",
email: "[email protected]",
password: "password123"
}
end
context "with valid parameters" do
it "creates a user" do
result = described_class.new(valid_params).call
expect(result.success?).to be true
expect(result.user).to be_persisted
end
it "creates a profile for the user" do
result = described_class.new(valid_params).call
expect(result.user.profile).to be_present
end
it "sends a welcome email" do
expect {
described_class.new(valid_params).call
}.to have_enqueued_mail(UserMailer, :welcome)
end
end
context "with invalid parameters" do
let(:invalid_params) { { email: "invalid" } }
it "returns a failure result" do
result = described_class.new(invalid_params).call
expect(result.failure?).to be true
expect(result.errors).to be_present
end
it "does not create a user" do
expect {
described_class.new(invalid_params).call
}.not_to change(User, :count)
end
end
end
end
```
## Shared Examples
Reusable Test Patterns:
```ruby
# spec/support/shared_examples/api_resource.rb
RSpec.shared_examples "an API resource" do
it "returns JSON content type" do
expect(response.content_type).to include("application/json")
end
it "returns a successful response" do
expect(response).to have_http_status(:ok)
end
end
# Usage in specs
RSpec.describe "Users API", type: :request do
describe "GET /api/users" do
before { get api_users_path }
it_behaves_like "an API resource"
end
end
```
## Mocking and Stubbing
External Service Mocking:
```ruby
RSpec.describe PaymentService do
describe "#charge" do
let(:payment_gateway) { instance_double(Stripe::PaymentIntent) }
before do
allow(Stripe::PaymentIntent).to receive(:create).and_return(payment_gateway)
allow(payment_gateway).to receive(:status).and_return("succeeded")
end
it "processes the payment successfully" do
result = described_class.new.charge(amount: 1000, customer_id: "cus_123")
expect(result).to be_success
end
end
end
```
```