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:
markdown
1## Model: [ModelName]
2
3### Table: [table_name]
4
5### Attributes
67|------|------|-------------|---------|
8| name | string | required, unique | - |
9| email | string | required, unique, email format | - |
10| status | integer | enum | 0 (pending) |
11| organization_id | bigint | foreign key | - |
12
13### Associations
14- belongs_to :organization
15- has_many :posts, dependent: :destroy
16- has_one :profile, dependent: :destroy
17
18### Validations
19- name: presence, uniqueness, length(max: 100)
20- email: presence, uniqueness, format(email)
21- status: inclusion in enum values
22
23### Scopes
24- active: status = active
25- recent: ordered by created_at desc
26- by_organization(org): where organization_id = org.id
27
28### Instance Methods
29- full_name: combines first_name and last_name
30- active?: checks if status is active
31
32### Callbacks
33- before_save :normalize_email
34- after_create :send_welcome_email
Step 2: Create Model Spec
Location: spec/models/[model_name]_spec.rb
ruby
1# frozen_string_literal: true
2
3require 'rails_helper'
4
5RSpec.describe ModelName, type: :model do
6 subject { build(:model_name) }
7
8 # === Associations ===
9 describe 'associations' do
10 it { is_expected.to belong_to(:organization) }
11 it { is_expected.to have_many(:posts).dependent(:destroy) }
12 end
13
14 # === Validations ===
15 describe 'validations' do
16 it { is_expected.to validate_presence_of(:name) }
17 it { is_expected.to validate_uniqueness_of(:email).case_insensitive }
18 it { is_expected.to validate_length_of(:name).is_at_most(100) }
19 end
20
21 # === Scopes ===
22 describe '.active' do
23 let!(:active_record) { create(:model_name, status: :active) }
24 let!(:inactive_record) { create(:model_name, status: :inactive) }
25
26 it 'returns only active records' do
27 expect(described_class.active).to include(active_record)
28 expect(described_class.active).not_to include(inactive_record)
29 end
30 end
31
32 # === Instance Methods ===
33 describe '#full_name' do
34 subject { build(:model_name, first_name: 'John', last_name: 'Doe') }
35
36 it 'returns combined name' do
37 expect(subject.full_name).to eq('John Doe')
38 end
39 end
40end
See templates/model_spec.erb for full template.
Step 3: Create Factory
Location: spec/factories/[model_name_plural].rb
ruby
1# frozen_string_literal: true
2
3FactoryBot.define do
4 factory :model_name do
5 sequence(:name) { |n| "Name #{n}" }
6 sequence(:email) { |n| "user#{n}@example.com" }
7 status { :pending }
8 association :organization
9
10 trait :active do
11 status { :active }
12 end
13
14 trait :with_posts do
15 after(:create) do |record|
16 create_list(:post, 3, model_name: record)
17 end
18 end
19 end
20end
See templates/factory.erb for full template.
Step 4: Run Spec (Verify RED)
bash
1bundle exec rspec spec/models/model_name_spec.rb
Expected: Failure because model/table doesn't exist.
Step 5: Generate Migration
bash
1bin/rails generate migration CreateModelNames \
2 name:string \
3 email:string:uniq \
4 status:integer \
5 organization:references
Review the generated migration and add:
- Null constraints:
null: false
- Defaults:
default: 0
- Indexes:
add_index :table, :column
ruby
1# db/migrate/YYYYMMDDHHMMSS_create_model_names.rb
2class CreateModelNames < ActiveRecord::Migration[8.0]
3 def change
4 create_table :model_names do |t|
5 t.string :name, null: false
6 t.string :email, null: false
7 t.integer :status, null: false, default: 0
8 t.references :organization, null: false, foreign_key: true
9
10 t.timestamps
11 end
12
13 add_index :model_names, :email, unique: true
14 add_index :model_names, :status
15 end
16end
Step 6: Run Migration
bash
1bin/rails db:migrate
Verify with:
bash
1bin/rails db:migrate:status
Step 7: Create Model File
Location: app/models/[model_name].rb
ruby
1# frozen_string_literal: true
2
3class ModelName < ApplicationRecord
4end
Step 8: Run Spec (Still RED)
bash
1bundle exec rspec spec/models/model_name_spec.rb
Expected: Failures for missing validations/associations.
Step 9: Add Validations & Associations
ruby
1# frozen_string_literal: true
2
3class ModelName < ApplicationRecord
4 # === Associations ===
5 belongs_to :organization
6 has_many :posts, dependent: :destroy
7
8 # === Enums ===
9 enum :status, { pending: 0, active: 1, suspended: 2 }
10
11 # === Validations ===
12 validates :name, presence: true,
13 uniqueness: true,
14 length: { maximum: 100 }
15 validates :email, presence: true,
16 uniqueness: { case_sensitive: false },
17 format: { with: URI::MailTo::EMAIL_REGEXP }
18
19 # === Scopes ===
20 scope :active, -> { where(status: :active) }
21 scope :recent, -> { order(created_at: :desc) }
22
23 # === Instance Methods ===
24 def full_name
25 "#{first_name} #{last_name}".strip
26 end
27end
Step 10: Run Spec (GREEN)
bash
1bundle exec rspec spec/models/model_name_spec.rb
All specs should pass.
References
Common Patterns
Enum with Validation
ruby
1enum :status, { draft: 0, published: 1, archived: 2 }
2validates :status, inclusion: { in: statuses.keys }
Polymorphic Association
ruby
1belongs_to :commentable, polymorphic: true
Counter Cache
ruby
1belongs_to :organization, counter_cache: true
2# Add: organization.posts_count column
Soft Delete
ruby
1scope :active, -> { where(deleted_at: nil) }
2scope :deleted, -> { where.not(deleted_at: nil) }
3
4def soft_delete
5 update(deleted_at: Time.current)
6end