# Ruby on Rails Framework — SunLint Agent Guide

> Priority directives for AI agents working on Rails projects.
> Rule files: `.agent/skills/sunlint-code-quality/rules/`

---

## Critical Patterns — Apply Every Time

### Input Filtering
- **Always** whitelist params with `params.require(:model).permit(:field1, :field2)`
- **Never** use `params[:model].to_unsafe_h`, `params.permit!`, or `Model.create(params[:model])`
- See: `RR001-strong-parameters.md`

### N+1 Queries
- Any controller loading a collection that renders association data → add `includes(:relation)`
- Use `eager_load(:relation)` when filtering by association columns (SQL `WHERE`)
- Count in loops → use `counter_cache: true` + `model.relation_count` column
- See: `RR002-eager-load-includes.md`, `RR011-counter-cache.md`

### Authentication
- `before_action :authenticate_user!` in `ApplicationController` — opt-out for public actions with `skip_before_action`
- **Never** add authentication checks per-action (opt-in) — new actions are public by default
- See: `RR008-before-action-auth.md`

### Secrets
- **Never** hardcode API keys, tokens, passwords in source files
- `Rails.application.credentials.section[:key]` OR `ENV.fetch('KEY')` only
- `config/master.key` and `config/credentials/*.key` must be in `.gitignore`
- See: `RR009-rails-credentials.md`

---

## Architecture Patterns

### Controller responsibility — thin controllers only
Controllers do exactly: authenticate → authorize → parse strong params → call service → render response.
```ruby
def create
  result = PlaceOrderService.new(user: current_user, params: order_params).call
  if result.success?
    render json: OrderResource.new(result.order), status: :created
  else
    render json: { errors: result.errors }, status: :unprocessable_entity
  end
end
```
See: `RR003-service-objects.md`

### Service Objects
- Business logic with multi-model writes, external calls, or branching → `app/services/` PORO
- Expose one public method: `#call`
- Return a `Result` struct: `Result = Struct.new(:value, :success?, :error, keyword_init: true)`
- See: `RR003-service-objects.md`

### Background Jobs
- Email, external API calls, file processing, report generation → `ActiveJob` + `perform_later`
- **Never** `Thread.new` in controllers, **never** inline slow work in the request cycle
- Pass only primitive/serializable arguments to jobs (IDs, strings) — not AR objects
- See: `RR004-active-job-background.md`

### Pagination
- Every collection endpoint must call `.page(params[:page]).per(n)` (Kaminari) or `pagy(...)` (Pagy)
- **Never** `Model.all` without a page limit returned to the client
- See: `RR005-pagination.md`

### Bulk Data Iteration (jobs/scripts/rake)
- `Model.all.each` → replace with `Model.find_each(batch_size: 500)`
- Bulk updates → `Model.in_batches(of: 200) { |batch| batch.update_all(...) }`
- See: `RR006-find-each-batches.md`

### Query Scopes
- Named conditions (`published`, `recent`, `by_author`) → `scope :name, -> { where(...) }` on model
- **Never** inline `where('...')` chains in controllers — define a scope, call the scope
- See: `RR010-scopes.md`

### HTTP Status Codes
- Every `render json:` must include `status:` — use Rails symbol names
- `POST` success → `:created` (201), validation failure → `:unprocessable_entity` (422)
- DELETE with no body → `head :no_content` (204)
- Centralize `rescue_from ActiveRecord::RecordNotFound` → `:not_found` in ApplicationController
- See: `RR007-http-status-codes.md`, `RR012-render-json-status.md`

---

## Run Commands

```bash
rails generate model   ModelName field:type
rails generate service OrderService          # custom generator if installed
rails generate job     ProcessExportJob
rails generate migration AddCounterToTable

rails test test/models/post_test.rb          # run specific test file
rails test -n /test_name_pattern/            # run by name match
bundle exec rspec spec/services/             # RSpec equivalent

rails credentials:edit --environment production   # edit secrets
rails credentials:show                            # view decrypted

bundle exec rubocop --autocorrect            # lint + fix
bundle exec brakeman                         # security scanner
bundle exec bundle-audit check               # dependency CVE scanner
```

---

## What NOT to do (quick reference)

| ❌ Wrong | ✅ Correct |
|---|---|
| `params[:user].to_unsafe_h` | `params.require(:user).permit(:name, :email)` |
| `Model.create(params[:model])` | `Model.create(user_params)` (permitted) |
| `@posts = Post.all` (unbounded) | `@posts = Post.published.page(params[:page])` |
| `post.comments.count` in loop | `counter_cache: true` → `post.comments_count` |
| `Thread.new { ... }` | `MyJob.perform_later(...)` |
| `after_create :call_stripe` on model | Service object wrapping the transaction |
| Auth check per action (opt-in) | `before_action :authenticate_user!` in ApplicationController |
| `render json: { errors: ... }` | `render json: { errors: ... }, status: :unprocessable_entity` |
| `API_KEY = 'sk_live_...'` in source | `Rails.application.credentials.stripe[:api_key]` |
| Inline `where('published = true')` in controller | `scope :published, -> { where(published: true) }` |
