--- name: rails-model-generator description: Creates Rails models using TDD approach - spec first, then migration, then model. Use when creating new models, adding model validations, defining associations, or setting up database tables. allowed-tools: Read, Write, Edit, Bash --- # Rails Model Generator (TDD Approach) ## Overview This skill creates models the TDD way: 1. Define requirements (attributes, validations, associations) 2. Write model spec with expected behavior (RED) 3. Create factory for test data 4. Generate migration 5. Implement model to pass specs (GREEN) 6. 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 ## 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 Spec Location: `spec/models/[model_name]_spec.rb` ```ruby # 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 ``` See [templates/model_spec.erb](templates/model_spec.erb) for full template. ## Step 3: Create Factory Location: `spec/factories/[model_name_plural].rb` ```ruby # 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 ``` See [templates/factory.erb](templates/factory.erb) for full template. ## Step 4: Run Spec (Verify RED) ```bash bundle exec rspec spec/models/model_name_spec.rb ``` Expected: Failure because model/table doesn't exist. ## Step 5: Generate Migration ```bash 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` ```ruby # 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 ```bash bin/rails db:migrate ``` Verify with: ```bash bin/rails db:migrate:status ``` ## Step 7: Create Model File Location: `app/models/[model_name].rb` ```ruby # frozen_string_literal: true class ModelName < ApplicationRecord end ``` ## Step 8: Run Spec (Still RED) ```bash bundle exec rspec spec/models/model_name_spec.rb ``` Expected: Failures for missing validations/associations. ## Step 9: Add Validations & Associations ```ruby # 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) ```bash bundle exec rspec spec/models/model_name_spec.rb ``` All specs should pass. ## References - See [templates/model_spec.erb](templates/model_spec.erb) for spec template - See [templates/factory.erb](templates/factory.erb) for factory template - See [reference/validations.md](reference/validations.md) for validation patterns ## Common Patterns ### Enum with Validation ```ruby enum :status, { draft: 0, published: 1, archived: 2 } validates :status, inclusion: { in: statuses.keys } ``` ### Polymorphic Association ```ruby belongs_to :commentable, polymorphic: true ``` ### Counter Cache ```ruby belongs_to :organization, counter_cache: true # Add: organization.posts_count column ``` ### Soft Delete ```ruby scope :active, -> { where(deleted_at: nil) } scope :deleted, -> { where.not(deleted_at: nil) } def soft_delete update(deleted_at: Time.current) end ```