mirror of
https://github.com/ryankazokas/turbovault-app.git
synced 2026-04-16 22:12:53 +00:00
Adds types
This commit is contained in:
307
docs/CI_PIPELINE.md
Normal file
307
docs/CI_PIPELINE.md
Normal file
@@ -0,0 +1,307 @@
|
||||
# CI/CD Pipeline Documentation
|
||||
|
||||
TurboVault uses GitHub Actions for continuous integration and quality assurance.
|
||||
|
||||
## Pipeline Overview
|
||||
|
||||
The CI pipeline runs on:
|
||||
- ✅ Every push to `main` or `develop` branches
|
||||
- ✅ Every pull request to `main` or `develop`
|
||||
|
||||
## Jobs
|
||||
|
||||
### 1. Linting & Security 🔍
|
||||
|
||||
**Purpose:** Code style and security checks
|
||||
|
||||
**Steps:**
|
||||
1. **RuboCop** - Ruby style guide enforcement
|
||||
- Uses `rubocop-rails-omakase` (Rails official style guide)
|
||||
- Runs with `--parallel` for speed
|
||||
- **Fails build** if style violations found
|
||||
|
||||
2. **Brakeman** - Security vulnerability scanner
|
||||
- Scans for Rails security issues
|
||||
- `continue-on-error: true` (warnings don't block PRs)
|
||||
|
||||
**Run locally:**
|
||||
```bash
|
||||
task lint # Check style
|
||||
task lint:fix # Auto-fix issues
|
||||
task security # Run Brakeman
|
||||
```
|
||||
|
||||
### 2. Type Checking 🔷
|
||||
|
||||
**Purpose:** Static type checking with Sorbet
|
||||
|
||||
**Steps:**
|
||||
1. Generate RBI files for gems and Rails DSLs
|
||||
2. Run Sorbet type checker
|
||||
3. **Fails build** if type errors found
|
||||
|
||||
**Run locally:**
|
||||
```bash
|
||||
task tapioca:all # Generate RBI files
|
||||
task typecheck # Run type checker
|
||||
```
|
||||
|
||||
**Note:** RBI generation can take 1-2 minutes in CI.
|
||||
|
||||
### 3. Tests 🧪
|
||||
|
||||
**Purpose:** Run test suite
|
||||
|
||||
**Services:**
|
||||
- PostgreSQL 15 database
|
||||
|
||||
**Steps:**
|
||||
1. Set up PostgreSQL service
|
||||
2. Install system dependencies
|
||||
3. Create test database
|
||||
4. Load schema
|
||||
5. Run test suite
|
||||
|
||||
**Run locally:**
|
||||
```bash
|
||||
task test # Run all tests
|
||||
```
|
||||
|
||||
### 4. Docker Build 🐳
|
||||
|
||||
**Purpose:** Verify Docker image builds successfully
|
||||
|
||||
**Steps:**
|
||||
1. Set up Docker Buildx
|
||||
2. Build production Docker image
|
||||
3. Test that image runs
|
||||
|
||||
**Run locally:**
|
||||
```bash
|
||||
docker build -t turbovault:test .
|
||||
docker run --rm turbovault:test bundle exec ruby --version
|
||||
```
|
||||
|
||||
## CI Workflow File
|
||||
|
||||
Location: `.github/workflows/ci.yml`
|
||||
|
||||
## Status Badges
|
||||
|
||||
Status badges in `README.md`:
|
||||
|
||||
```markdown
|
||||

|
||||
[](https://github.com/rubocop/rubocop)
|
||||
[](https://sorbet.org)
|
||||
```
|
||||
|
||||
**Don't forget** to replace `YOUR_USERNAME` with your actual GitHub username!
|
||||
|
||||
## What Fails the Build?
|
||||
|
||||
| Check | Fails Build? | Why |
|
||||
|-------|--------------|-----|
|
||||
| RuboCop style issues | ✅ Yes | Code style should be consistent |
|
||||
| Brakeman security warnings | ❌ No | Some warnings are false positives |
|
||||
| Sorbet type errors | ✅ Yes | Type safety is important |
|
||||
| Test failures | ✅ Yes | Tests must pass |
|
||||
| Docker build failure | ✅ Yes | Must be deployable |
|
||||
|
||||
## Fixing CI Failures
|
||||
|
||||
### RuboCop Failures
|
||||
|
||||
```bash
|
||||
# Auto-fix most issues
|
||||
task lint:fix
|
||||
|
||||
# Check what's left
|
||||
task lint
|
||||
|
||||
# Commit fixes
|
||||
git add .
|
||||
git commit -m "Fix RuboCop style issues"
|
||||
```
|
||||
|
||||
### Sorbet Type Errors
|
||||
|
||||
```bash
|
||||
# Regenerate RBI files (if gems changed)
|
||||
task tapioca:all
|
||||
|
||||
# Check for errors
|
||||
task typecheck
|
||||
|
||||
# Fix type errors in code
|
||||
# See docs/SORBET.md for patterns
|
||||
```
|
||||
|
||||
### Test Failures
|
||||
|
||||
```bash
|
||||
# Run tests locally
|
||||
task test
|
||||
|
||||
# Run specific test
|
||||
rails test test/models/game_test.rb
|
||||
|
||||
# Fix tests and re-run
|
||||
```
|
||||
|
||||
### Docker Build Failures
|
||||
|
||||
```bash
|
||||
# Build locally to debug
|
||||
docker build -t turbovault:test .
|
||||
|
||||
# Check build logs for errors
|
||||
# Usually missing dependencies or incorrect Dockerfile
|
||||
```
|
||||
|
||||
## CI Performance
|
||||
|
||||
Typical run times:
|
||||
- **Linting & Security:** ~30-60 seconds
|
||||
- **Type Checking:** ~2-3 minutes (RBI generation)
|
||||
- **Tests:** ~1-2 minutes
|
||||
- **Docker Build:** ~3-5 minutes
|
||||
|
||||
**Total:** ~7-11 minutes per run
|
||||
|
||||
## Optimizations
|
||||
|
||||
### Caching
|
||||
|
||||
We use `bundler-cache: true` in Ruby setup to cache gems:
|
||||
```yaml
|
||||
- uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
bundler-cache: true
|
||||
```
|
||||
|
||||
This speeds up runs by ~1-2 minutes.
|
||||
|
||||
### Parallel Execution
|
||||
|
||||
Jobs run in parallel, not sequentially:
|
||||
- Linting, Type Checking, Tests, and Docker Build all run simultaneously
|
||||
- Total time = slowest job (usually Docker Build)
|
||||
|
||||
### RuboCop Parallel
|
||||
|
||||
RuboCop uses `--parallel` to check files concurrently:
|
||||
```bash
|
||||
bundle exec rubocop --parallel
|
||||
```
|
||||
|
||||
## Local Development Workflow
|
||||
|
||||
Before pushing:
|
||||
|
||||
```bash
|
||||
# 1. Fix style
|
||||
task lint:fix
|
||||
|
||||
# 2. Check types
|
||||
task typecheck
|
||||
|
||||
# 3. Run tests
|
||||
task test
|
||||
|
||||
# 4. Commit
|
||||
git add .
|
||||
git commit -m "Your changes"
|
||||
git push
|
||||
```
|
||||
|
||||
This ensures CI will pass!
|
||||
|
||||
## Skipping CI
|
||||
|
||||
To skip CI on a commit (e.g., documentation only):
|
||||
|
||||
```bash
|
||||
git commit -m "Update README [skip ci]"
|
||||
```
|
||||
|
||||
## CI for Pull Requests
|
||||
|
||||
When someone submits a PR:
|
||||
1. CI runs automatically
|
||||
2. All jobs must pass before merge
|
||||
3. Status shown in PR page
|
||||
4. Merge button disabled until CI passes
|
||||
|
||||
## Viewing CI Results
|
||||
|
||||
**On GitHub:**
|
||||
1. Go to repository
|
||||
2. Click **Actions** tab
|
||||
3. Click on a workflow run
|
||||
4. View each job's logs
|
||||
|
||||
**In Pull Requests:**
|
||||
- See status checks at bottom of PR
|
||||
- Click "Details" to view logs
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "RBI generation failed"
|
||||
|
||||
**Cause:** Tapioca couldn't generate RBI files
|
||||
|
||||
**Fix:** Usually transient - retry workflow. If persistent, check Gemfile.lock is committed.
|
||||
|
||||
### "Database connection failed"
|
||||
|
||||
**Cause:** PostgreSQL service not ready
|
||||
|
||||
**Fix:** Already handled with health checks:
|
||||
```yaml
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
```
|
||||
|
||||
### "Bundler version mismatch"
|
||||
|
||||
**Cause:** Different Bundler versions locally vs CI
|
||||
|
||||
**Fix:** Run `bundle update --bundler` locally
|
||||
|
||||
## Advanced: Matrix Builds (Future)
|
||||
|
||||
To test multiple Ruby versions:
|
||||
|
||||
```yaml
|
||||
strategy:
|
||||
matrix:
|
||||
ruby-version: ['3.2', '3.3', '3.4']
|
||||
```
|
||||
|
||||
Currently we only test Ruby 3.3.
|
||||
|
||||
## Security
|
||||
|
||||
### Secrets
|
||||
|
||||
We use GitHub Secrets for sensitive data:
|
||||
- `RAILS_MASTER_KEY` (optional, for encrypted credentials)
|
||||
- No other secrets needed for tests
|
||||
|
||||
### Dependabot
|
||||
|
||||
Consider enabling Dependabot to auto-update dependencies:
|
||||
- Settings → Security → Dependabot
|
||||
|
||||
## Resources
|
||||
|
||||
- [GitHub Actions Docs](https://docs.github.com/en/actions)
|
||||
- [RuboCop](https://github.com/rubocop/rubocop)
|
||||
- [Sorbet](https://sorbet.org/)
|
||||
- [Brakeman](https://brakemanscanner.org/)
|
||||
|
||||
---
|
||||
|
||||
**Questions?** Check the workflow file: `.github/workflows/ci.yml`
|
||||
@@ -95,6 +95,28 @@ task lint:fix
|
||||
task security
|
||||
```
|
||||
|
||||
### Type Checking with Sorbet
|
||||
|
||||
TurboVault uses Sorbet for gradual static type checking.
|
||||
|
||||
```bash
|
||||
# First time setup (after bundle install)
|
||||
task tapioca:init
|
||||
task tapioca:all
|
||||
|
||||
# Run type checker
|
||||
task typecheck
|
||||
|
||||
# Watch mode (re-checks on file changes)
|
||||
task typecheck:watch
|
||||
|
||||
# Update type definitions after gem/model changes
|
||||
task tapioca:gems # After bundle install
|
||||
task tapioca:dsl # After model changes
|
||||
```
|
||||
|
||||
**See [SORBET.md](SORBET.md) for complete type checking guide.**
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
|
||||
@@ -12,12 +12,14 @@ Complete documentation for TurboVault - Video Game Collection Tracker
|
||||
### Deployment & Development
|
||||
- [Deployment Guide](DEPLOYMENT.md) - Complete deployment reference
|
||||
- [Development Guide](DEVELOPMENT_GUIDE.md) - Local development & contributing
|
||||
- [CI Pipeline](CI_PIPELINE.md) - GitHub Actions & quality checks
|
||||
- [Kubernetes README](../k8s/README.md) - Kubernetes deployment
|
||||
|
||||
### Features
|
||||
### Features & Development
|
||||
- [API Documentation](API_DOCUMENTATION.md) - RESTful API reference
|
||||
- [IGDB Integration](IGDB_INTEGRATION.md) - Game metadata matching
|
||||
- [Themes](THEMES.md) - Theme customization
|
||||
- [Sorbet Type Checking](SORBET.md) - Gradual static types
|
||||
|
||||
### Configuration
|
||||
- [GitHub Secrets Setup](../.github/SECRETS_SETUP.md) - Optional custom registry
|
||||
|
||||
520
docs/SORBET.md
Normal file
520
docs/SORBET.md
Normal file
@@ -0,0 +1,520 @@
|
||||
# Sorbet Type Checking Guide
|
||||
|
||||
TurboVault uses [Sorbet](https://sorbet.org/) for gradual static type checking.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### First Time Setup
|
||||
|
||||
After installing gems, initialize Sorbet:
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
bundle install
|
||||
|
||||
# Initialize Tapioca (generates type definitions)
|
||||
task tapioca:init
|
||||
|
||||
# Generate RBI files for gems and Rails DSLs
|
||||
task tapioca:all
|
||||
```
|
||||
|
||||
This creates:
|
||||
- `sorbet/` - Sorbet configuration
|
||||
- `sorbet/rbi/gems/` - Type definitions for gems (gitignored)
|
||||
- `sorbet/rbi/dsl/` - Generated types for Rails models, etc. (gitignored)
|
||||
|
||||
### Running Type Checks
|
||||
|
||||
```bash
|
||||
# Check all files
|
||||
task typecheck
|
||||
|
||||
# Watch mode (re-checks on file changes)
|
||||
task typecheck:watch
|
||||
|
||||
# Or use Sorbet directly
|
||||
bundle exec srb tc
|
||||
```
|
||||
|
||||
## Type Strictness Levels
|
||||
|
||||
Sorbet uses gradual typing with 5 strictness levels:
|
||||
|
||||
### `# typed: false` (Default - No Checking)
|
||||
```ruby
|
||||
# typed: false
|
||||
class MyClass
|
||||
def do_something(x)
|
||||
x + 1 # No type checking at all
|
||||
end
|
||||
end
|
||||
```
|
||||
- **Use for:** Legacy code, external gems, code you're not ready to type yet
|
||||
- **Checking:** None
|
||||
|
||||
### `# typed: true` (Recommended)
|
||||
```ruby
|
||||
# typed: true
|
||||
class User < ApplicationRecord
|
||||
extend T::Sig
|
||||
|
||||
sig { returns(String) }
|
||||
def theme_class
|
||||
"theme-#{theme}"
|
||||
end
|
||||
end
|
||||
```
|
||||
- **Use for:** Most application code
|
||||
- **Checking:** Method signatures you define with `sig`
|
||||
|
||||
### `# typed: strict` (Advanced)
|
||||
- **Use for:** Critical business logic, libraries
|
||||
- **Checking:** All method signatures required, no untyped code
|
||||
|
||||
### `# typed: strong` (Expert)
|
||||
- **Use for:** Maximum safety
|
||||
- **Checking:** No `T.untyped`, no runtime checks
|
||||
|
||||
## Writing Type Signatures
|
||||
|
||||
### Basic Method Signatures
|
||||
|
||||
```ruby
|
||||
# typed: true
|
||||
class Game < ApplicationRecord
|
||||
extend T::Sig
|
||||
|
||||
# No parameters, returns String
|
||||
sig { returns(String) }
|
||||
def title
|
||||
read_attribute(:title)
|
||||
end
|
||||
|
||||
# One parameter, returns Boolean
|
||||
sig { params(user: User).returns(T::Boolean) }
|
||||
def owned_by?(user)
|
||||
self.user_id == user.id
|
||||
end
|
||||
|
||||
# Multiple parameters
|
||||
sig { params(title: String, platform: Platform).returns(Game) }
|
||||
def self.create_game(title, platform)
|
||||
create(title: title, platform: platform)
|
||||
end
|
||||
|
||||
# Void (returns nothing meaningful)
|
||||
sig { void }
|
||||
def update_cache
|
||||
Rails.cache.write("game_#{id}", self)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Nilable Types
|
||||
|
||||
```ruby
|
||||
# typed: true
|
||||
class Game < ApplicationRecord
|
||||
extend T::Sig
|
||||
|
||||
# Can return Platform or nil
|
||||
sig { returns(T.nilable(Platform)) }
|
||||
def platform
|
||||
super
|
||||
end
|
||||
|
||||
# Parameter can be nil
|
||||
sig { params(platform_id: T.nilable(Integer)).returns(T.untyped) }
|
||||
def self.by_platform(platform_id)
|
||||
where(platform_id: platform_id) if platform_id
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Arrays and Hashes
|
||||
|
||||
```ruby
|
||||
# typed: true
|
||||
class IgdbService
|
||||
extend T::Sig
|
||||
|
||||
# Array of Hashes
|
||||
sig { params(title: String).returns(T::Array[T::Hash[Symbol, T.untyped]]) }
|
||||
def search_game(title)
|
||||
# Returns: [{id: 1, name: "Game"}, {id: 2, name: "Game 2"}]
|
||||
end
|
||||
|
||||
# Array of specific type
|
||||
sig { returns(T::Array[Game]) }
|
||||
def all_games
|
||||
Game.all.to_a
|
||||
end
|
||||
|
||||
# Hash with specific keys
|
||||
sig { returns(T::Hash[String, Integer]) }
|
||||
def game_counts
|
||||
{ "total" => 100, "completed" => 50 }
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Optional Parameters
|
||||
|
||||
```ruby
|
||||
# typed: true
|
||||
class IgdbService
|
||||
extend T::Sig
|
||||
|
||||
# Optional parameter with default
|
||||
sig { params(title: String, limit: Integer).returns(T::Array[T.untyped]) }
|
||||
def search_game(title, limit = 3)
|
||||
# ...
|
||||
end
|
||||
|
||||
# Optional keyword parameter
|
||||
sig { params(title: String, platform: T.nilable(String)).returns(T::Array[T.untyped]) }
|
||||
def search(title, platform: nil)
|
||||
# ...
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Instance Variables
|
||||
|
||||
```ruby
|
||||
# typed: true
|
||||
class IgdbService
|
||||
extend T::Sig
|
||||
|
||||
sig { void }
|
||||
def initialize
|
||||
@client_id = T.let(ENV.fetch("IGDB_CLIENT_ID"), String)
|
||||
@client_secret = T.let(ENV.fetch("IGDB_CLIENT_SECRET"), String)
|
||||
@access_token = T.let(get_or_refresh_token, String)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
`T.let(value, Type)` declares the type of an instance variable.
|
||||
|
||||
### Untyped Code
|
||||
|
||||
When you can't determine the type (ActiveRecord relations, dynamic code):
|
||||
|
||||
```ruby
|
||||
# typed: true
|
||||
class GamesController < ApplicationController
|
||||
extend T::Sig
|
||||
|
||||
sig { void }
|
||||
def index
|
||||
# ActiveRecord relations are complex, use T.untyped
|
||||
@games = T.let(current_user.games.includes(:platform), T.untyped)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Use `T.untyped` sparingly - it bypasses type checking.
|
||||
|
||||
### CSV Rows (Common Pattern)
|
||||
|
||||
CSV::Row acts like a Hash but Sorbet doesn't know that:
|
||||
|
||||
```ruby
|
||||
# typed: true
|
||||
sig { void }
|
||||
def bulk_create
|
||||
csv = CSV.parse(csv_text, headers: true)
|
||||
|
||||
csv.each_with_index do |row, index|
|
||||
# Tell Sorbet row is untyped to avoid Array vs Hash confusion
|
||||
row = T.let(row, T.untyped)
|
||||
|
||||
# Now you can access row["column"] without errors
|
||||
title = row["title"]
|
||||
platform = row["platform"]
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
This is acceptable because CSV parsing is inherently dynamic and validated by your model.
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Controllers
|
||||
|
||||
```ruby
|
||||
# typed: true
|
||||
class GamesController < ApplicationController
|
||||
extend T::Sig
|
||||
|
||||
sig { void }
|
||||
def index
|
||||
@games = T.let(current_user.games.all, T.untyped)
|
||||
end
|
||||
|
||||
sig { void }
|
||||
def show
|
||||
@game = T.let(current_user.games.find(params[:id]), Game)
|
||||
end
|
||||
|
||||
sig { void }
|
||||
def create
|
||||
@game = T.let(current_user.games.build(game_params), Game)
|
||||
if @game.save
|
||||
redirect_to @game
|
||||
else
|
||||
render :new
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Models
|
||||
|
||||
```ruby
|
||||
# typed: true
|
||||
class Game < ApplicationRecord
|
||||
extend T::Sig
|
||||
|
||||
# Class methods
|
||||
sig { params(query: String).returns(T.untyped) }
|
||||
def self.search(query)
|
||||
where("title ILIKE ?", "%#{query}%")
|
||||
end
|
||||
|
||||
# Instance methods
|
||||
sig { returns(String) }
|
||||
def display_title
|
||||
"#{title} (#{platform&.name || 'Unknown'})"
|
||||
end
|
||||
|
||||
# Private methods
|
||||
private
|
||||
|
||||
sig { void }
|
||||
def set_defaults
|
||||
self.date_added ||= Date.current
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Services
|
||||
|
||||
```ruby
|
||||
# typed: true
|
||||
class IgdbService
|
||||
extend T::Sig
|
||||
|
||||
sig { void }
|
||||
def initialize
|
||||
@client_id = T.let(ENV.fetch("IGDB_CLIENT_ID"), String)
|
||||
@access_token = T.let(get_token, String)
|
||||
end
|
||||
|
||||
sig { params(title: String, platform: T.nilable(String)).returns(T::Array[T::Hash[Symbol, T.untyped]]) }
|
||||
def search_game(title, platform: nil)
|
||||
results = post("/games", build_query(title, platform))
|
||||
format_results(results)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
sig { params(title: String, platform: T.nilable(String)).returns(String) }
|
||||
def build_query(title, platform)
|
||||
"search \"#{title}\";"
|
||||
end
|
||||
|
||||
sig { params(results: T::Array[T.untyped]).returns(T::Array[T::Hash[Symbol, T.untyped]]) }
|
||||
def format_results(results)
|
||||
results.map { |r| { id: r["id"], name: r["name"] } }
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Gradual Adoption Strategy
|
||||
|
||||
### Phase 1: Infrastructure (Current)
|
||||
- ✅ Add Sorbet gems
|
||||
- ✅ Generate RBI files
|
||||
- ✅ Add `# typed: true` to key files
|
||||
- ✅ Type critical methods (User, Game, IgdbService)
|
||||
|
||||
### Phase 2: Core Models (Next)
|
||||
- Add types to all models
|
||||
- Type associations and scopes
|
||||
- Type validations and callbacks
|
||||
|
||||
### Phase 3: Controllers
|
||||
- Add types to controller actions
|
||||
- Type params and filters
|
||||
- Type helper methods
|
||||
|
||||
### Phase 4: Services & Jobs
|
||||
- Type all service objects
|
||||
- Type background jobs
|
||||
- Type API clients
|
||||
|
||||
### Phase 5: Increase Strictness
|
||||
- Move files from `typed: true` to `typed: strict`
|
||||
- Remove `T.untyped` where possible
|
||||
- Add runtime type checks where needed
|
||||
|
||||
## IDE Integration
|
||||
|
||||
### VSCode
|
||||
|
||||
Install the [Sorbet extension](https://marketplace.visualstudio.com/items?itemName=sorbet.sorbet-vscode-extension):
|
||||
|
||||
1. Install extension
|
||||
2. Sorbet will auto-detect your project
|
||||
3. Get inline type errors and autocomplete
|
||||
|
||||
### RubyMine
|
||||
|
||||
Sorbet support is built-in:
|
||||
|
||||
1. Enable Sorbet in Settings → Languages & Frameworks → Ruby
|
||||
2. RubyMine will use Sorbet for type checking
|
||||
|
||||
## Updating Types
|
||||
|
||||
When you add gems or change models:
|
||||
|
||||
```bash
|
||||
# After adding/updating gems
|
||||
task tapioca:gems
|
||||
|
||||
# After changing models/routes/etc
|
||||
task tapioca:dsl
|
||||
|
||||
# Both at once
|
||||
task tapioca:all
|
||||
```
|
||||
|
||||
Run this after:
|
||||
- `bundle install` (new/updated gems)
|
||||
- Adding/changing models
|
||||
- Adding/changing routes
|
||||
- Adding/changing concerns
|
||||
|
||||
## Common Errors
|
||||
|
||||
### "Parent of class redefined" (RDoc, etc.)
|
||||
|
||||
```
|
||||
Parent of class RDoc::Markup::Heading redefined from RDoc::Markup::Element to Struct
|
||||
```
|
||||
|
||||
**Fix:** This is a known gem incompatibility. Already fixed in `sorbet/config`:
|
||||
```
|
||||
--suppress-payload-superclass-redefinition-for=RDoc::Markup::Heading
|
||||
```
|
||||
|
||||
If you see similar errors for other gems, add suppression flags to `sorbet/config`.
|
||||
|
||||
### "Method does not exist"
|
||||
|
||||
```
|
||||
app/models/game.rb:10: Method `foo` does not exist on `Game`
|
||||
```
|
||||
|
||||
**Fix:** Add the method signature or check for typos.
|
||||
|
||||
### "Argument does not have asserted type"
|
||||
|
||||
```
|
||||
Expected String but got T.nilable(String)
|
||||
```
|
||||
|
||||
**Fix:** Handle nil case or use `T.must()`:
|
||||
|
||||
```ruby
|
||||
sig { params(name: String).void }
|
||||
def greet(name)
|
||||
puts "Hello #{name}"
|
||||
end
|
||||
|
||||
# Wrong
|
||||
greet(user.name) # name might be nil
|
||||
|
||||
# Right
|
||||
greet(user.name || "Guest")
|
||||
greet(T.must(user.name)) # Asserts non-nil
|
||||
```
|
||||
|
||||
### "Constants must have type annotations"
|
||||
|
||||
```ruby
|
||||
# Wrong
|
||||
MY_CONSTANT = "value"
|
||||
|
||||
# Right
|
||||
MY_CONSTANT = T.let("value", String)
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### ✅ DO:
|
||||
- Start with `# typed: true` (not strict)
|
||||
- Use `sig` for public methods
|
||||
- Type critical business logic
|
||||
- Use `T.untyped` for complex ActiveRecord queries
|
||||
- Update RBI files after gem/model changes
|
||||
- Run type checks in CI
|
||||
|
||||
### ❌ DON'T:
|
||||
- Type everything at once (gradual adoption!)
|
||||
- Use `# typed: strict` on new code (too strict)
|
||||
- Ignore type errors (fix or suppress with comment)
|
||||
- Forget to run `tapioca:dsl` after model changes
|
||||
|
||||
## CI Integration
|
||||
|
||||
Add to `.github/workflows/ci.yml`:
|
||||
|
||||
```yaml
|
||||
- name: Type check with Sorbet
|
||||
run: |
|
||||
bundle exec tapioca gems --no-doc
|
||||
bundle exec tapioca dsl
|
||||
bundle exec srb tc
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- [Sorbet Documentation](https://sorbet.org/)
|
||||
- [Sorbet Playground](https://sorbet.run/)
|
||||
- [Tapioca Documentation](https://github.com/Shopify/tapioca)
|
||||
- [T::Types Cheat Sheet](https://sorbet.org/docs/stdlib-generics)
|
||||
|
||||
## Current Status
|
||||
|
||||
**All files start with `# typed: false`** (no checking enabled yet).
|
||||
|
||||
**Ready to type (after running setup):**
|
||||
- `app/models/user.rb`
|
||||
- `app/models/game.rb`
|
||||
- `app/services/igdb_service.rb`
|
||||
- `app/controllers/games_controller.rb`
|
||||
|
||||
**Recommended typing order:**
|
||||
1. Start with Models: User, Game, Platform, Genre
|
||||
2. Then Services: IgdbService
|
||||
3. Then Controllers: GamesController, CollectionsController
|
||||
4. Finally Jobs and Helpers
|
||||
|
||||
**Important:** Run the setup steps first:
|
||||
```bash
|
||||
bundle install
|
||||
task tapioca:init
|
||||
task tapioca:all
|
||||
task typecheck # Should show "No errors!"
|
||||
```
|
||||
|
||||
Then start adding `# typed: true` and signatures incrementally.
|
||||
|
||||
---
|
||||
|
||||
**Happy typing!** 🎉 Start small, add types gradually, and enjoy better IDE support and bug catching!
|
||||
Reference in New Issue
Block a user