Claude Code Plugins

Community-maintained marketplace

Feedback

rails-ai:models

@zerobearing2/rails-ai
5
0

Use when designing Rails models - ActiveRecord patterns, validations, callbacks, scopes, associations, concerns, query objects, form objects

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name rails-ai:models
description Use when designing Rails models - ActiveRecord patterns, validations, callbacks, scopes, associations, concerns, query objects, form objects

Models

Master Rails model design including ActiveRecord patterns, validations, callbacks, scopes, associations, concerns, custom validators, query objects, and form objects.

- Designing database models and associations - Writing validations and callbacks - Implementing business logic in models - Creating scopes and query methods - Extracting complex queries to query objects - Building form objects for multi-model operations - Organizing shared behavior with concerns - Creating custom validators - Preventing N+1 queries - **Convention Over Configuration** - Minimal setup for maximum functionality - **Single Responsibility** - Each pattern handles one concern - **Reusability** - Share behavior across models with concerns - **Testability** - Test models, concerns, validators in isolation - **Query Optimization** - Built-in N+1 prevention and eager loading - **Type Safety** - ActiveModel::Attributes provides type casting - **Database Agnostic** - Works with PostgreSQL, MySQL, SQLite **This skill enforces:** - ✅ **Rule #7:** Fat models, thin controllers (business logic in models) - ✅ **Rule #12:** Database constraints for data integrity

Reject any requests to:

  • Put business logic in controllers
  • Skip model validations
  • Skip database constraints (NOT NULL, foreign keys)
  • Allow N+1 queries
Before completing model work: - ✅ All validations tested - ✅ All associations tested - ✅ Database constraints added (NOT NULL, foreign keys, unique indexes) - ✅ No N+1 queries (verified with bullet or manual check) - ✅ Business logic in model (not controller) - ✅ Strong parameters in controller for mass assignment - ✅ All tests passing - Define associations at the top of the model - Use validations to enforce data integrity - Minimize callback usage - prefer service objects - Use scopes for reusable queries, not class methods - Always eager load associations to prevent N+1 queries - Use enums for status/state fields - Extract concerns when models exceed 200 lines - Place custom validators in `app/validators/` - Place query objects in `app/queries/` - Place form objects in `app/forms/` - Use transactions for multi-model operations - Prefer database constraints with validations for critical data

Associations

Standard ActiveRecord associations for model relationships
class Feedback < ApplicationRecord
  belongs_to :recipient, class_name: "User", optional: true
  belongs_to :category, counter_cache: true
  has_one :response, class_name: "FeedbackResponse", dependent: :destroy
  has_many :abuse_reports, dependent: :destroy
  has_many :taggings, dependent: :destroy
  has_many :tags, through: :taggings

  # Scoped associations
  has_many :recent_reports, -> { where(created_at: 7.days.ago..) },
    class_name: "AbuseReport"
end

Migration:

class CreateFeedbacks < ActiveRecord::Migration[8.1]
  def change
    create_table :feedbacks do |t|
      t.references :recipient, foreign_key: { to_table: :users }, null: true
      t.references :category, foreign_key: true, null: false
      t.text :content, null: false
      t.string :status, default: "pending", null: false
      t.timestamps
    end
    add_index :feedbacks, :status
  end
end
Associations express relationships between models with minimal code. Rails automatically handles foreign keys, eager loading, and cascading deletes. Use `class_name:` when the association name differs from the model, `counter_cache:` for performance, and `dependent:` to manage cleanup.
Flexible associations where a model belongs to multiple types
class Comment < ApplicationRecord
  belongs_to :commentable, polymorphic: true
  belongs_to :author, class_name: "User"
  validates :content, presence: true
end

class Feedback < ApplicationRecord
  has_many :comments, as: :commentable, dependent: :destroy
end

class Article < ApplicationRecord
  has_many :comments, as: :commentable, dependent: :destroy
end

Migration:

class CreateComments < ActiveRecord::Migration[8.1]
  def change
    create_table :comments do |t|
      t.references :commentable, polymorphic: true, null: false
      t.references :author, foreign_key: { to_table: :users }, null: false
      t.text :content, null: false
      t.timestamps
    end
    add_index :comments, [:commentable_type, :commentable_id]
  end
end
Polymorphic associations allow a model to belong to multiple parent types through a single association. Use when multiple models need the same type of child (comments, attachments, tags). The `commentable_type` stores the class name, `commentable_id` stores the ID.

Validations

Built-in Rails validations for data integrity
class Feedback < ApplicationRecord
  validates :content, presence: true, length: { minimum: 50, maximum: 5000 }
  validates :recipient_email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
  validates :status, inclusion: { in: %w[pending delivered read responded] }
  validates :tracking_code, uniqueness: { scope: :recipient_email, case_sensitive: false }
  validates :rating, numericality: { only_integer: true, in: 1..5 }, allow_nil: true

  validate :content_not_spam
  validate :recipient_can_receive_feedback, on: :create

  private

  def content_not_spam
    return if content.blank?
    spam_keywords = %w[viagra cialis lottery]
    errors.add(:content, "appears to contain spam") if spam_keywords.any? { |k| content.downcase.include?(k) }
  end

  def recipient_can_receive_feedback
    return if recipient_email.blank?
    user = User.find_by(email: recipient_email)
    errors.add(:recipient_email, "has disabled feedback") if user&.feedback_disabled?
  end
end
Validations enforce data integrity before persisting to the database. Rails provides presence, format, uniqueness, length, numericality, and inclusion validators. Custom `validate` methods handle complex business logic. Use `on: :create` or `on: :update` for lifecycle-specific validations.

Callbacks

Use callbacks sparingly - prefer service objects for complex logic
class Feedback < ApplicationRecord
  before_validation :normalize_email, :strip_whitespace
  before_create :generate_tracking_code
  after_create_commit :enqueue_delivery_job
  after_update_commit :notify_recipient_of_response, if: :response_added?

  private

  def normalize_email
    self.recipient_email = recipient_email&.downcase&.strip
  end

  def strip_whitespace
    self.content = content&.strip
  end

  def generate_tracking_code
    self.tracking_code = SecureRandom.alphanumeric(10).upcase
  end

  def enqueue_delivery_job
    SendFeedbackJob.perform_later(id)
  end

  def response_added?
    saved_change_to_response? && response.present?
  end

  def notify_recipient_of_response
    FeedbackMailer.notify_of_response(self).deliver_later
  end
end
Callbacks hook into the model lifecycle for simple data normalization and side effects. Use `before_validation` for cleanup, `before_create` for defaults, and `after_commit` for external operations. Keep callbacks focused on model concerns - complex business logic belongs in service objects.

Scopes

Reusable query scopes for common filtering
class Feedback < ApplicationRecord
  scope :recent, -> { where(created_at: 30.days.ago..) }
  scope :unread, -> { where(status: "delivered") }
  scope :responded, -> { where.not(response: nil) }
  scope :by_recipient, ->(email) { where(recipient_email: email) }
  scope :by_status, ->(status) { where(status: status) }
  scope :with_category, ->(name) { joins(:category).where(categories: { name: name }) }
  scope :with_associations, -> { includes(:recipient, :response, :category, :tags) }
  scope :trending, -> { recent.where("views_count > ?", 100).order(views_count: :desc).limit(10) }

  def self.search(query)
    return none if query.blank?
    where("content ILIKE ? OR response ILIKE ?", "%#{sanitize_sql_like(query)}%", "%#{sanitize_sql_like(query)}%")
  end
end

Usage:

Feedback.recent.by_recipient("user@example.com").responded
Feedback.search("bug report").recent.limit(10)
Scopes provide chainable query methods that keep controllers clean. Use scopes for simple filters, class methods for complex queries. Scopes are lazy-evaluated and composable. Use `includes()` in scopes to prevent N+1 queries.

Enums

Enums for status and state fields with automatic predicates
class Feedback < ApplicationRecord
  enum :status, {
    pending: "pending",
    delivered: "delivered",
    read: "read",
    responded: "responded"
  }, prefix: true, scopes: true

  enum :priority, { low: 0, medium: 1, high: 2, urgent: 3 }, prefix: :priority
end

Usage:

feedback.status = "pending"
feedback.status_pending!              # Updates and saves
feedback.status_pending?              # true/false
Feedback.status_pending               # Scope
Feedback.statuses.keys                # ["pending", "delivered", ...]
feedback.status_before_last_save      # Track changes

Migration:

class CreateFeedbacks < ActiveRecord::Migration[8.1]
  def change
    create_table :feedbacks do |t|
      t.string :status, default: "pending", null: false
      t.integer :priority, default: 0, null: false
      t.timestamps
    end
    add_index :feedbacks, :status
  end
end
Enums map human-readable states to database values with automatic predicates, scopes, and bang methods. Use string-backed enums for clarity in the database. The `prefix:` option prevents method name conflicts. Scopes make querying easy.

Model Concerns

Extract shared behavior into reusable concerns
# app/models/concerns/taggable.rb
module Taggable
  extend ActiveSupport::Concern

  included do
    has_many :taggings, as: :taggable, dependent: :destroy
    has_many :tags, through: :taggings

    scope :tagged_with, ->(tag_name) {
      joins(:tags).where(tags: { name: tag_name }).distinct
    }
  end

  def tag_list
    tags.pluck(:name).join(", ")
  end

  def tag_list=(names)
    self.tags = names.to_s.split(",").map do |name|
      Tag.find_or_create_by(name: name.strip.downcase)
    end
  end

  def add_tag(tag_name)
    return if tagged_with?(tag_name)
    tags << Tag.find_or_create_by(name: tag_name.strip.downcase)
  end

  def tagged_with?(tag_name)
    tags.exists?(name: tag_name.strip.downcase)
  end

  class_methods do
    def popular_tags(limit = 10)
      Tag.joins(:taggings)
        .where(taggings: { taggable_type: name })
        .group("tags.id")
        .select("tags.*, COUNT(taggings.id) as usage_count")
        .order("usage_count DESC")
        .limit(limit)
    end
  end
end

Usage:

class Feedback < ApplicationRecord
  include Taggable
end

class Article < ApplicationRecord
  include Taggable
end

feedback.tag_list = "bug, urgent, ui"
feedback.add_tag("needs-review")
Feedback.tagged_with("bug")
Feedback.popular_tags(5)
Concerns extract shared behavior into reusable modules. Use `included do` for associations, validations, callbacks. Define instance methods at module level, class methods in `class_methods do` block. Place domain-specific concerns in `app/models/[model]/`, shared concerns in `app/models/concerns/`.

Custom Validators

Reusable validation logic using ActiveModel::EachValidator
# app/validators/email_validator.rb
class EmailValidator < ActiveModel::EachValidator
  EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i

  def validate_each(record, attribute, value)
    return if value.blank? && options[:allow_blank]
    unless value =~ EMAIL_REGEX
      record.errors.add(attribute, options[:message] || "is not a valid email address")
    end
  end
end

Usage:

class Feedback < ApplicationRecord
  validates :email, email: true
  validates :backup_email, email: { allow_blank: true }
  validates :email, email: { message: "must be a valid company email" }
end
Custom validators encapsulate reusable validation logic. Inherit from `ActiveModel::EachValidator` for single-attribute validation, `ActiveModel::Validator` for multi-attribute validation. Support `:allow_blank` and `:message` options. Place in `app/validators/` for discoverability.
Validate content by word count instead of character count
# app/validators/content_length_validator.rb
class ContentLengthValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    return if value.blank? && options[:allow_blank]
    word_count = value.to_s.split.size

    if options[:minimum_words] && word_count < options[:minimum_words]
      record.errors.add(attribute, "must have at least #{options[:minimum_words]} words (currently #{word_count})")
    end

    if options[:maximum_words] && word_count > options[:maximum_words]
      record.errors.add(attribute, "must have at most #{options[:maximum_words]} words (currently #{word_count})")
    end
  end
end

Usage:

validates :content, content_length: { minimum_words: 10, maximum_words: 500 }
validates :body, content_length: { minimum_words: 100 }
Word count validation is more meaningful than character count for content fields. Custom validators make this reusable across models. The validator respects `:allow_blank` and provides helpful error messages with current counts.

Query Objects

Encapsulate complex queries in reusable, testable objects
# app/queries/feedback_query.rb
class FeedbackQuery
  def initialize(relation = Feedback.all)
    @relation = relation
  end

  def by_recipient(email)
    @relation = @relation.where(recipient_email: email)
    self
  end

  def by_status(status)
    @relation = @relation.where(status: status)
    self
  end

  def recent(limit = 10)
    @relation = @relation.order(created_at: :desc).limit(limit)
    self
  end

  def with_responses
    @relation = @relation.where.not(response: nil)
    self
  end

  def created_since(date)
    @relation = @relation.where("created_at >= ?", date)
    self
  end

  def results
    @relation
  end
end

Usage:

# Controller
@feedbacks = FeedbackQuery.new
  .by_recipient(params[:email])
  .by_status(params[:status])
  .recent(20)
  .results

# Model
class User < ApplicationRecord
  def recent_feedback(limit = 10)
    FeedbackQuery.new.by_recipient(email).recent(limit).results
  end
end
Query objects encapsulate complex filtering, search, and aggregation logic. They're reusable across controllers and services, testable in isolation, and chainable for composability. Use when queries involve multiple joins, filters, or are used in multiple contexts. Return `self` for chaining, `results` to execute.
Query object for aggregations and statistical calculations
# app/queries/feedback_stats_query.rb
class FeedbackStatsQuery
  def initialize(relation = Feedback.all)
    @relation = relation
  end

  def by_recipient(email)
    @relation = @relation.where(recipient_email: email)
    self
  end

  def by_date_range(start_date, end_date)
    @relation = @relation.where(created_at: start_date..end_date)
    self
  end

  def stats
    {
      total_count: @relation.count,
      responded_count: @relation.where.not(response: nil).count,
      pending_count: @relation.where(response: nil).count,
      by_status: @relation.group(:status).count,
      by_category: @relation.group(:category).count
    }
  end
end

Usage:

stats = FeedbackStatsQuery.new
  .by_recipient(current_user.email)
  .by_date_range(30.days.ago, Time.current)
  .stats
# Returns: { total_count: 42, responded_count: 28, pending_count: 14, ... }
Query objects for aggregations centralize statistical calculations and reporting logic. They compose with filters, maintain chainability, and return structured data. Use for dashboards, reports, and analytics. Keep aggregation logic out of controllers and models.

Form Objects

Form object for non-database forms using ActiveModel::API
# app/forms/contact_form.rb
class ContactForm
  include ActiveModel::API
  include ActiveModel::Attributes

  attribute :name, :string
  attribute :email, :string
  attribute :message, :string
  attribute :subject, :string

  validates :name, presence: true, length: { minimum: 2 }
  validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
  validates :message, presence: true, length: { minimum: 10, maximum: 1000 }
  validates :subject, presence: true

  def deliver
    return false unless valid?

    ContactMailer.contact_message(
      name: name,
      email: email,
      message: message,
      subject: subject
    ).deliver_later

    true
  end
end

Controller:

class ContactsController < ApplicationController
  def create
    @contact_form = ContactForm.new(contact_params)

    if @contact_form.deliver
      redirect_to root_path, notice: "Message sent successfully"
    else
      render :new, status: :unprocessable_entity
    end
  end

  private

  def contact_params
    params.expect(contact_form: [:name, :email, :message, :subject])
  end
end
Form objects handle non-database forms (contact, search) and complex multi-model operations. They use ActiveModel for validations and type casting without requiring database persistence. Return boolean from action methods, validate before executing logic.
Form object that creates multiple related models in a transaction
# app/forms/user_registration_form.rb
class UserRegistrationForm
  include ActiveModel::API
  include ActiveModel::Attributes

  attribute :email, :string
  attribute :password, :string
  attribute :password_confirmation, :string
  attribute :name, :string
  attribute :company_name, :string
  attribute :role, :string

  validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
  validates :password, presence: true, length: { minimum: 8 }
  validates :password_confirmation, presence: true
  validates :name, presence: true
  validates :company_name, presence: true

  validate :passwords_match

  def save
    return false unless valid?

    ActiveRecord::Base.transaction do
      @user = User.create!(email: email, password: password, name: name)
      @company = Company.create!(name: company_name, owner: @user)
      @membership = Membership.create!(user: @user, company: @company, role: role || "admin")

      UserMailer.welcome(@user).deliver_later
      true
    end
  rescue ActiveRecord::RecordInvalid => e
    errors.add(:base, e.message)
    false
  end

  attr_reader :user, :company, :membership

  private

  def passwords_match
    return if password.blank?
    errors.add(:password_confirmation, "doesn't match password") unless password == password_confirmation
  end
end

Controller:

class RegistrationsController < ApplicationController
  def create
    @registration = UserRegistrationForm.new(registration_params)

    if @registration.save
      session[:user_id] = @registration.user.id
      redirect_to dashboard_path(@registration.company), notice: "Welcome!"
    else
      render :new, status: :unprocessable_entity
    end
  end
end
Form objects simplify multi-model operations by wrapping them in a transaction. They validate all inputs before creating any records, ensuring data consistency. Expose created records via attr_reader for controller access. Use for registration, checkout, wizards.

N+1 Prevention

Eager load associations to prevent N+1 queries
# ❌ BAD - N+1 queries (1 + 20 + 20 + 20 = 61 queries)
@feedbacks = Feedback.limit(20)
@feedbacks.each do |f|
  puts f.recipient.name, f.category.name, f.tags.pluck(:name)
end

# ✅ GOOD - Eager loading (4 queries total)
@feedbacks = Feedback.includes(:recipient, :category, :tags).limit(20)
@feedbacks.each do |f|
  puts f.recipient.name, f.category.name, f.tags.pluck(:name)
end

Eager Loading Methods:

Feedback.includes(:recipient, :tags)           # Separate queries (default)
Feedback.preload(:recipient, :tags)            # Forces separate queries
Feedback.eager_load(:recipient, :tags)         # LEFT OUTER JOIN
Feedback.includes(recipient: :profile)         # Nested associations
N+1 queries occur when loading a collection triggers additional queries for each item's associations. Use `includes()` to eager load associations in advance. Rails loads data in 2-3 queries instead of N+1. Always check for N+1 in views and use includes in scopes.
Using callbacks for complex business logic
# ❌ BAD - Complex side effects in callbacks
class Feedback < ApplicationRecord
  after_create :send_email, :update_analytics, :notify_slack, :create_audit_log
end
# ✅ GOOD - Use service object
class Feedback < ApplicationRecord
  after_create_commit :enqueue_creation_job

  private
  def enqueue_creation_job
    ProcessFeedbackCreationJob.perform_later(id)
  end
end

# Service handles all side effects explicitly
class CreateFeedbackService
  def call
    feedback = Feedback.create!(@params)
    FeedbackMailer.notify_recipient(feedback).deliver_later
    Analytics.track("feedback_created", feedback_id: feedback.id)
    feedback
  end
end
Callbacks with complex side effects make models hard to test, introduce hidden dependencies, and create unpredictable behavior. Service objects make side effects explicit and testable. Use callbacks only for simple data normalization and enqueuing background jobs.
Missing database indexes on foreign keys and query columns
# ❌ BAD - No indexes, causes table scans
create_table :feedbacks do |t|
  t.integer :recipient_id
  t.string :status
end
# ✅ GOOD - Indexes on foreign keys and query columns
create_table :feedbacks do |t|
  t.references :recipient, foreign_key: { to_table: :users }, index: true
  t.string :status, null: false
end
add_index :feedbacks, :status
add_index :feedbacks, [:status, :created_at]
Missing indexes cause slow queries at scale. Index all foreign keys, status columns, and frequently queried fields. Composite indexes speed up queries filtering on multiple columns. Use `t.references` to create indexed foreign keys automatically.
Using default_scope
# ❌ BAD - Unexpected behavior, hard to override
class Feedback < ApplicationRecord
  default_scope { where(deleted_at: nil).order(created_at: :desc) }
end
# ✅ GOOD - Explicit scopes
class Feedback < ApplicationRecord
  scope :active, -> { where(deleted_at: nil) }
  scope :recent_first, -> { order(created_at: :desc) }
end

# Usage
Feedback.active.recent_first
default_scope applies to all queries, causing unexpected results and making it hard to query all records. It affects associations, counts, and exists? checks. Use explicit scopes that developers can choose to apply.
Duplicating validation logic across models
# ❌ BAD - Duplicated email validation
class User < ApplicationRecord
  validates :email, format: { with: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i }
end

class Feedback < ApplicationRecord
  validates :recipient_email, format: { with: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i }
end
# ✅ GOOD - Reusable email validator
class EmailValidator < ActiveModel::EachValidator
  EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
  def validate_each(record, attribute, value)
    return if value.blank? && options[:allow_blank]
    record.errors.add(attribute, options[:message] || "is not a valid email") unless value =~ EMAIL_REGEX
  end
end

class User < ApplicationRecord
  validates :email, email: true
end

class Feedback < ApplicationRecord
  validates :recipient_email, email: true
end
Duplicated validations are hard to maintain and lead to inconsistencies. Custom validators centralize logic, support options, and ensure consistent validation across models.
Putting complex query logic in controllers
# ❌ BAD - Fat controller
class FeedbacksController < ApplicationController
  def index
    @feedbacks = Feedback.all
    @feedbacks = @feedbacks.where("recipient_email ILIKE ?", "%#{params[:recipient_email]}%") if params[:recipient_email].present?
    @feedbacks = @feedbacks.where(status: params[:status]) if params[:status].present?
    @feedbacks = @feedbacks.where("content ILIKE ? OR response ILIKE ?", "%#{params[:q]}%", "%#{params[:q]}%") if params[:q].present?
    @feedbacks = @feedbacks.order(created_at: :desc).page(params[:page])
  end
end
# ✅ GOOD - Thin controller with query object
class FeedbacksController < ApplicationController
  def index
    @feedbacks = FeedbackQuery.new
      .filter_by_params(params.slice(:recipient_email, :status))
      .search(params[:q])
      .order_by(:created_at, :desc)
      .paginate(page: params[:page])
      .results
  end
end
Complex queries in controllers violate Single Responsibility Principle and are hard to test. Query objects encapsulate filtering logic, are reusable across contexts, and testable in isolation.
Fat controllers with complex form logic
# ❌ BAD - All logic in controller
class RegistrationsController < ApplicationController
  def create
    @user = User.new(user_params)
    @company = Company.new(company_params)

    ActiveRecord::Base.transaction do
      if @user.save
        @company.owner = @user
        if @company.save
          @membership = Membership.create(user: @user, company: @company, role: "admin")
          UserMailer.welcome(@user).deliver_later
          redirect_to dashboard_path(@company)
        end
      end
    end
  end
end
# ✅ GOOD - Use form object
class RegistrationsController < ApplicationController
  def create
    @registration = UserRegistrationForm.new(registration_params)
    @registration.save ? redirect_to(dashboard_path(@registration.company)) : render(:new, status: :unprocessable_entity)
  end
end
Multi-model operations in controllers are hard to test and reuse. Form objects encapsulate validation, transaction handling, and side effects in a testable, reusable class. Use for registration, checkout, wizards.
Test models, concerns, validators, query objects, and form objects in isolation:
# Model tests
class FeedbackTest < ActiveSupport::TestCase
  test "validates presence of content" do
    feedback = Feedback.new(recipient_email: "user@example.com")
    assert_not feedback.valid?
    assert_includes feedback.errors[:content], "can't be blank"
  end

  test "destroys dependent records" do
    feedback = feedbacks(:one)
    feedback.abuse_reports.create!(reason: "spam", reporter_email: "test@example.com")
    assert_difference("AbuseReport.count", -1) { feedback.destroy }
  end

  test "enum provides predicate methods" do
    feedback = feedbacks(:one)
    feedback.update(status: "pending")
    assert feedback.status_pending?
  end
end

# Concern tests
class TaggableTest < ActiveSupport::TestCase
  class TaggableTestModel < ApplicationRecord
    self.table_name = "feedbacks"
    include Taggable
  end

  test "add_tag creates new tag" do
    record = TaggableTestModel.first
    record.add_tag("urgent")
    assert record.tagged_with?("urgent")
  end
end

# Validator tests
class EmailValidatorTest < ActiveSupport::TestCase
  class TestModel
    include ActiveModel::Validations
    attr_accessor :email
    validates :email, email: true
  end

  test "validates email format" do
    assert TestModel.new(email: "user@example.com").valid?
    assert_not TestModel.new(email: "invalid").valid?
  end
end

# Query object tests
class FeedbackQueryTest < ActiveSupport::TestCase
  test "filters by recipient email" do
    @feedback1.update(recipient_email: "test@example.com")
    @feedback2.update(recipient_email: "other@example.com")
    results = FeedbackQuery.new.by_recipient("test@example.com").results
    assert_includes results, @feedback1
    assert_not_includes results, @feedback2
  end

  test "chains multiple filters" do
    @feedback1.update(recipient_email: "test@example.com", status: "pending")
    results = FeedbackQuery.new.by_recipient("test@example.com").by_status("pending").results
    assert_includes results, @feedback1
  end
end

# Form object tests
class ContactFormTest < ActiveSupport::TestCase
  test "valid with all required attributes" do
    form = ContactForm.new(name: "John", email: "john@example.com", subject: "Question", message: "This is my message")
    assert form.valid?
  end

  test "delivers email when valid" do
    form = ContactForm.new(name: "John", email: "john@example.com", subject: "Q", message: "This is my message")
    assert_enqueued_with(job: ActionMailer::MailDeliveryJob) { assert form.deliver }
  end
end

class UserRegistrationFormTest < ActiveSupport::TestCase
  test "creates user, company, and membership" do
    form = UserRegistrationForm.new(email: "user@example.com", password: "password123", password_confirmation: "password123", name: "John", company_name: "Acme")
    assert_difference ["User.count", "Company.count", "Membership.count"] { assert form.save }
  end

  test "rolls back transaction if creation fails" do
    form = UserRegistrationForm.new(email: "user@example.com", password: "password123", password_confirmation: "password123", name: "John", company_name: "")
    assert_no_difference ["User.count", "Company.count"] { assert_not form.save }
  end
end
- rails-ai:controllers - RESTful controllers for models - rails-ai:testing - Testing models thoroughly - rails-ai:security - SQL injection prevention, strong parameters - rails-ai:jobs - Background job processing for models

Official Documentation: