rails-model-generator
SKILL.md
Rails Model Generator (TDD Approach)
Overview
This skill creates models the TDD way:
- Define requirements (attributes, validations, associations)
- Write model test with expected behavior (RED)
- Create fixtures for test data
- Generate migration
- Implement model to pass tests (GREEN)
- Refactor if needed
Workflow Checklist
Model Creation Progress:
- [ ] Step 1: Define requirements (attributes, validations, associations)
- [ ] Step 2: Create model test (RED)
- [ ] Step 3: Create fixtures
- [ ] Step 4: Run test (should fail - no model/table)
- [ ] Step 5: Generate migration
- [ ] Step 6: Run migration
- [ ] Step 7: Create model file (empty)
- [ ] Step 8: Run test (should fail - no validations)
- [ ] Step 9: Add validations and associations
- [ ] Step 10: Run test (GREEN)
Step 1: Requirements Template
Before writing code, define the model:
## Model: [ModelName]
### Table: [table_name]
### Attributes
| Name | Type | Constraints | Default |
|------|------|-------------|---------|
| name | string | required, unique | - |
| email | string | required, unique, email format | - |
| status | integer | enum | 0 (pending) |
| organization_id | bigint | foreign key | - |
### Associations
- belongs_to :organization
- has_many :posts, dependent: :destroy
- has_one :profile, dependent: :destroy
### Validations
- name: presence, uniqueness, length(max: 100)
- email: presence, uniqueness, format(email)
- status: inclusion in enum values
### Scopes
- active: status = active
- recent: ordered by created_at desc
- by_organization(org): where organization_id = org.id
### Instance Methods
- full_name: combines first_name and last_name
- active?: checks if status is active
### Callbacks
- before_save :normalize_email
- after_create :send_welcome_email
Step 2: Create Model Test
Location: test/models/[model_name]_test.rb
# frozen_string_literal: true
require "test_helper"
class ModelNameTest < ActiveSupport::TestCase
# === Associations ===
test "belongs to organization" do
model = model_names(:one)
assert_respond_to model, :organization
assert_instance_of Organization, model.organization
end
test "has many posts" do
model = model_names(:one)
assert_respond_to model, :posts
end
# === Validations ===
test "requires name" do
model = ModelName.new(name: nil)
assert_not model.valid?
assert_includes model.errors[:name], "can't be blank"
end
test "requires unique email (case insensitive)" do
existing = model_names(:one)
model = ModelName.new(email: existing.email.upcase)
assert_not model.valid?
assert_includes model.errors[:email], "has already been taken"
end
test "validates name length max 100" do
model = ModelName.new(name: "a" * 101)
assert_not model.valid?
assert model.errors[:name].any? { |e| e.include?("too long") }
end
# === Scopes ===
test ".active returns only active records" do
active_record = model_names(:active_one)
inactive_record = model_names(:inactive_one)
results = ModelName.active
assert_includes results, active_record
assert_not_includes results, inactive_record
end
# === Instance Methods ===
test "#full_name returns combined name" do
model = ModelName.new(first_name: "John", last_name: "Doe")
assert_equal "John Doe", model.full_name
end
end
Step 3: Create Fixtures
Location: test/fixtures/[model_name_plural].yml
# test/fixtures/model_names.yml
one:
name: "Test Model One"
email: "model-one@example.com"
status: 0
organization: one
two:
name: "Test Model Two"
email: "model-two@example.com"
status: 0
organization: one
active_one:
name: "Active Model"
email: "active@example.com"
status: 1
organization: one
inactive_one:
name: "Inactive Model"
email: "inactive@example.com"
status: 2
organization: one
Step 4: Run Test (Verify RED)
bin/rails test test/models/model_name_test.rb
Expected: Failure because model/table doesn't exist.
Step 5: Generate Migration
bin/rails generate migration CreateModelNames \
name:string \
email:string:uniq \
status:integer \
organization:references
Review the generated migration and add:
- Null constraints:
null: false - Defaults:
default: 0 - Indexes:
add_index :table, :column
# db/migrate/YYYYMMDDHHMMSS_create_model_names.rb
class CreateModelNames < ActiveRecord::Migration[8.0]
def change
create_table :model_names do |t|
t.string :name, null: false
t.string :email, null: false
t.integer :status, null: false, default: 0
t.references :organization, null: false, foreign_key: true
t.timestamps
end
add_index :model_names, :email, unique: true
add_index :model_names, :status
end
end
Step 6: Run Migration
bin/rails db:migrate
Verify with:
bin/rails db:migrate:status
Step 7: Create Model File
Location: app/models/[model_name].rb
# frozen_string_literal: true
class ModelName < ApplicationRecord
end
Step 8: Run Test (Still RED)
bin/rails test test/models/model_name_test.rb
Expected: Failures for missing validations/associations.
Step 9: Add Validations & Associations
# frozen_string_literal: true
class ModelName < ApplicationRecord
# === Associations ===
belongs_to :organization
has_many :posts, dependent: :destroy
# === Enums ===
enum :status, { pending: 0, active: 1, suspended: 2 }
# === Validations ===
validates :name, presence: true,
uniqueness: true,
length: { maximum: 100 }
validates :email, presence: true,
uniqueness: { case_sensitive: false },
format: { with: URI::MailTo::EMAIL_REGEXP }
# === Scopes ===
scope :active, -> { where(status: :active) }
scope :recent, -> { order(created_at: :desc) }
# === Instance Methods ===
def full_name
"#{first_name} #{last_name}".strip
end
end
Step 10: Run Test (GREEN)
bin/rails test test/models/model_name_test.rb
All tests should pass.
References
- See reference/validations.md for validation patterns
Common Patterns
Enum with Validation
enum :status, { draft: 0, published: 1, archived: 2 }
validates :status, inclusion: { in: statuses.keys }
Polymorphic Association
belongs_to :commentable, polymorphic: true
Counter Cache
belongs_to :organization, counter_cache: true
# Add: organization.posts_count column
Soft Delete
scope :active, -> { where(deleted_at: nil) }
scope :deleted, -> { where.not(deleted_at: nil) }
def soft_delete
update(deleted_at: Time.current)
end
Normalizes (Rails 7.1+)
normalizes :email, with: -> { _1.strip.downcase }
normalizes :phone, with: -> { _1.gsub(/\D/, "") }
Weekly Installs
2
Repository
dchuk/rails_ai_agentsFirst Seen
7 days ago
Security Audits
Installed on
opencode2
gemini-cli2
antigravity2
claude-code2
windsurf2
codex2