Rails Model Generator (TDD Approach)
Overview
This skill creates models the TDD way:
-
Define requirements (attributes, validations, associations)
-
Write model spec with expected behavior (RED)
-
Create factory for test data
-
Generate migration
-
Implement model to pass specs (GREEN)
-
Refactor if needed
Workflow Checklist
Model Creation Progress:
- Step 1: Define requirements (attributes, validations, associations)
- Step 2: Create model spec (RED)
- Step 3: Create factory
- Step 4: Run spec (should fail - no model/table)
- Step 5: Generate migration
- Step 6: Run migration
- Step 7: Create model file (empty)
- Step 8: Run spec (should fail - no validations)
- Step 9: Add validations and associations
- Step 10: Run spec (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 | - |
| 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 Spec
Location: spec/models/[model_name]_spec.rb
frozen_string_literal: true
require 'rails_helper'
RSpec.describe ModelName, type: :model do subject { build(:model_name) }
=== Associations ===
describe 'associations' do it { is_expected.to belong_to(:organization) } it { is_expected.to have_many(:posts).dependent(:destroy) } end
=== Validations ===
describe 'validations' do it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_uniqueness_of(:email).case_insensitive } it { is_expected.to validate_length_of(:name).is_at_most(100) } end
=== Scopes ===
describe '.active' do let!(:active_record) { create(:model_name, status: :active) } let!(:inactive_record) { create(:model_name, status: :inactive) }
it 'returns only active records' do
expect(described_class.active).to include(active_record)
expect(described_class.active).not_to include(inactive_record)
end
end
=== Instance Methods ===
describe '#full_name' do subject { build(:model_name, first_name: 'John', last_name: 'Doe') }
it 'returns combined name' do
expect(subject.full_name).to eq('John Doe')
end
end end
Step 3: Create Factory
Location: spec/factories/[model_name_plural].rb
frozen_string_literal: true
FactoryBot.define do factory :model_name do sequence(:name) { |n| "Name #{n}" } sequence(:email) { |n| "user#{n}@example.com" } status { :pending } association :organization
trait :active do
status { :active }
end
trait :with_posts do
after(:create) do |record|
create_list(:post, 3, model_name: record)
end
end
end end
Step 4: Run Spec (Verify RED)
bundle exec rspec spec/models/model_name_spec.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 Spec (Still RED)
bundle exec rspec spec/models/model_name_spec.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 Spec (GREEN)
bundle exec rspec spec/models/model_name_spec.rb
All specs 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