Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
dchuk avatar

Rails Model Generator

  • 2 installs
  • Updated February 9, 2026
  • dchuk/rails_ai_agents

Creates Rails models the TDD way: test first, then migration, then model, including validations and associations.

About

Generates Rails models with a TDD flow of test, migration, then model. A developer uses it when creating models, adding validations, or defining associations and tables.

  • Test-first, then migration, then model
  • Validations and associations setup

Rails Model Generator by the numbers

  • 2 all-time installs (skills.sh)
  • Ranked #743 of 911 Databases skills by installs in the Skillselion catalog
  • Data as of Jul 29, 2026 (Skillselion catalog sync)
npx skills add https://github.com/dchuk/rails_ai_agents --skill rails-model-generator

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs2
Last updatedFebruary 9, 2026
Repositorydchuk/rails_ai_agents

What it does

Creates Rails models the TDD way: test first, then migration, then model, including validations and associations.

Files

SKILL.mdMarkdownGitHub ↗

Rails Model Generator (TDD Approach)

Overview

This skill creates models the TDD way: 1. Define requirements (attributes, validations, associations) 2. Write model test with expected behavior (RED) 3. Create fixtures for test data 4. Generate migration 5. Implement model to pass tests (GREEN) 6. 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/, "") }

Related skills

Databasesdatabases

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.