Initial commit
This commit is contained in:
763
commands/ruby-optimize.md
Normal file
763
commands/ruby-optimize.md
Normal file
@@ -0,0 +1,763 @@
|
||||
---
|
||||
description: Analyze and optimize Ruby code for performance, memory usage, and idiomatic patterns
|
||||
---
|
||||
|
||||
# Ruby Optimize Command
|
||||
|
||||
Analyzes Ruby code and provides optimization recommendations for performance, memory usage, code readability, and idiomatic Ruby patterns.
|
||||
|
||||
## Arguments
|
||||
|
||||
- **$1: path** (required) - File or directory path to optimize
|
||||
- **$2: focus** (optional) - Optimization focus: `performance`, `memory`, `readability`, or `all` (default: `all`)
|
||||
|
||||
## Usage Examples
|
||||
|
||||
```bash
|
||||
# Analyze and optimize all aspects
|
||||
/ruby-optimize app/models/user.rb
|
||||
|
||||
# Focus on performance only
|
||||
/ruby-optimize app/services/ performance
|
||||
|
||||
# Focus on memory optimization
|
||||
/ruby-optimize lib/data_processor.rb memory
|
||||
|
||||
# Focus on readability and idioms
|
||||
/ruby-optimize app/ readability
|
||||
|
||||
# Optimize entire project
|
||||
/ruby-optimize . all
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Profile and Analyze Code
|
||||
|
||||
**Discovery Phase:**
|
||||
|
||||
1. Parse Ruby files in specified path
|
||||
2. Identify methods and code patterns
|
||||
3. Detect performance anti-patterns
|
||||
4. Analyze memory allocation patterns
|
||||
5. Check for idiomatic Ruby usage
|
||||
6. Measure complexity metrics
|
||||
|
||||
**Analysis Tools:**
|
||||
```ruby
|
||||
# Use Ruby parser
|
||||
require 'parser/current'
|
||||
|
||||
# AST analysis for pattern detection
|
||||
ast = Parser::CurrentRuby.parse(source_code)
|
||||
|
||||
# Complexity analysis
|
||||
require 'flog'
|
||||
flog = Flog.new
|
||||
flog.flog(file_path)
|
||||
```
|
||||
|
||||
### Step 2: Performance Analysis
|
||||
|
||||
**Detect Performance Anti-Patterns:**
|
||||
|
||||
**1. Inefficient Enumeration:**
|
||||
```ruby
|
||||
# ISSUE: Using each when map is appropriate
|
||||
def process_users
|
||||
result = []
|
||||
users.each do |user|
|
||||
result << user.name.upcase
|
||||
end
|
||||
result
|
||||
end
|
||||
|
||||
# OPTIMIZED: Use map
|
||||
def process_users
|
||||
users.map { |user| user.name.upcase }
|
||||
end
|
||||
|
||||
# Benchmark improvement: 15-20% faster, less memory
|
||||
```
|
||||
|
||||
**2. Repeated Object Creation:**
|
||||
```ruby
|
||||
# ISSUE: Creating regex in loop
|
||||
def filter_emails(emails)
|
||||
emails.select { |email| email.match(/@gmail\.com/) }
|
||||
end
|
||||
|
||||
# OPTIMIZED: Create regex once
|
||||
EMAIL_PATTERN = /@gmail\.com/
|
||||
|
||||
def filter_emails(emails)
|
||||
emails.select { |email| email.match(EMAIL_PATTERN) }
|
||||
end
|
||||
|
||||
# Benchmark improvement: 30-40% faster for large datasets
|
||||
```
|
||||
|
||||
**3. N+1 Query Detection:**
|
||||
```ruby
|
||||
# ISSUE: N+1 queries
|
||||
def user_with_posts
|
||||
users = User.all
|
||||
users.map do |user|
|
||||
{
|
||||
name: user.name,
|
||||
posts_count: user.posts.count # Separate query for each user
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
# OPTIMIZED: Eager load or use counter cache
|
||||
def user_with_posts
|
||||
users = User.eager(:posts).all
|
||||
users.map do |user|
|
||||
{
|
||||
name: user.name,
|
||||
posts_count: user.posts.count
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
# Or with counter cache
|
||||
def user_with_posts
|
||||
users = User.all
|
||||
users.map do |user|
|
||||
{
|
||||
name: user.name,
|
||||
posts_count: user.posts_count # From counter cache
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
# Benchmark improvement: 10-100x faster depending on data size
|
||||
```
|
||||
|
||||
**4. Inefficient String Building:**
|
||||
```ruby
|
||||
# ISSUE: String concatenation in loop
|
||||
def build_csv(records)
|
||||
csv = ""
|
||||
records.each do |record|
|
||||
csv += "#{record.id},#{record.name}\n"
|
||||
end
|
||||
csv
|
||||
end
|
||||
|
||||
# OPTIMIZED: Use array join or StringIO
|
||||
def build_csv(records)
|
||||
records.map { |r| "#{r.id},#{r.name}" }.join("\n")
|
||||
end
|
||||
|
||||
# Or for very large datasets
|
||||
require 'stringio'
|
||||
|
||||
def build_csv(records)
|
||||
StringIO.new.tap do |io|
|
||||
records.each do |record|
|
||||
io.puts "#{record.id},#{record.name}"
|
||||
end
|
||||
end.string
|
||||
end
|
||||
|
||||
# Benchmark improvement: 5-10x faster for large datasets
|
||||
```
|
||||
|
||||
**5. Unnecessary Sorting:**
|
||||
```ruby
|
||||
# ISSUE: Sorting entire collection when only need max/min
|
||||
def highest_score(users)
|
||||
users.sort_by(&:score).last
|
||||
end
|
||||
|
||||
# OPTIMIZED: Use max_by
|
||||
def highest_score(users)
|
||||
users.max_by(&:score)
|
||||
end
|
||||
|
||||
# Benchmark improvement: O(n) vs O(n log n)
|
||||
```
|
||||
|
||||
**6. Block Performance:**
|
||||
```ruby
|
||||
# ISSUE: Symbol#to_proc with arguments
|
||||
users.map { |u| u.name.upcase }
|
||||
|
||||
# OPTIMIZED: Use method chaining where possible
|
||||
users.map(&:name).map(&:upcase)
|
||||
|
||||
# ISSUE: Creating proc in loop
|
||||
items.select { |item| item.active? }
|
||||
|
||||
# OPTIMIZED: Use symbol to_proc
|
||||
items.select(&:active?)
|
||||
|
||||
# Benchmark improvement: 10-15% faster
|
||||
```
|
||||
|
||||
**7. Hash Access Patterns:**
|
||||
```ruby
|
||||
# ISSUE: Checking key and accessing value separately
|
||||
if hash.key?(:name)
|
||||
value = hash[:name]
|
||||
process(value)
|
||||
end
|
||||
|
||||
# OPTIMIZED: Use fetch or safe navigation
|
||||
if value = hash[:name]
|
||||
process(value)
|
||||
end
|
||||
|
||||
# Or with default
|
||||
value = hash.fetch(:name, default_value)
|
||||
process(value)
|
||||
|
||||
# ISSUE: Using Hash#merge in loop
|
||||
result = {}
|
||||
items.each do |item|
|
||||
result = result.merge(item.to_hash)
|
||||
end
|
||||
|
||||
# OPTIMIZED: Use Hash#merge! or each_with_object
|
||||
result = items.each_with_object({}) do |item, hash|
|
||||
hash.merge!(item.to_hash)
|
||||
end
|
||||
|
||||
# Benchmark improvement: 2-3x faster
|
||||
```
|
||||
|
||||
### Step 3: Memory Optimization
|
||||
|
||||
**Detect Memory Issues:**
|
||||
|
||||
**1. String Allocation:**
|
||||
```ruby
|
||||
# ISSUE: Creating new strings in loop
|
||||
1000.times do
|
||||
hash['key'] = value # Creates new 'key' string each time
|
||||
end
|
||||
|
||||
# OPTIMIZED: Use symbols or frozen strings
|
||||
1000.times do
|
||||
hash[:key] = value # Reuses same symbol
|
||||
end
|
||||
|
||||
# Or with frozen string literal
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Memory saved: ~40 bytes per string
|
||||
```
|
||||
|
||||
**2. Array/Hash Allocation:**
|
||||
```ruby
|
||||
# ISSUE: Building large array without size hint
|
||||
data = []
|
||||
10_000.times do |i|
|
||||
data << i
|
||||
end
|
||||
|
||||
# OPTIMIZED: Preallocate size
|
||||
data = Array.new(10_000)
|
||||
10_000.times do |i|
|
||||
data[i] = i
|
||||
end
|
||||
|
||||
# Or use a different approach
|
||||
data = (0...10_000).to_a
|
||||
|
||||
# Memory improvement: Fewer reallocations
|
||||
```
|
||||
|
||||
**3. Object Copying:**
|
||||
```ruby
|
||||
# ISSUE: Unnecessary duplication
|
||||
def process(data)
|
||||
temp = data.dup
|
||||
temp.map! { |item| item * 2 }
|
||||
temp
|
||||
end
|
||||
|
||||
# OPTIMIZED: Use map without dup if original not needed
|
||||
def process(data)
|
||||
data.map { |item| item * 2 }
|
||||
end
|
||||
|
||||
# Memory saved: Full array copy avoided
|
||||
```
|
||||
|
||||
**4. Lazy Evaluation:**
|
||||
```ruby
|
||||
# ISSUE: Loading everything into memory
|
||||
File.readlines('large_file.txt').each do |line|
|
||||
process(line)
|
||||
end
|
||||
|
||||
# OPTIMIZED: Process line by line
|
||||
File.foreach('large_file.txt') do |line|
|
||||
process(line)
|
||||
end
|
||||
|
||||
# Or use lazy enumeration
|
||||
File.readlines('large_file.txt').lazy.each do |line|
|
||||
process(line)
|
||||
end
|
||||
|
||||
# Memory saved: File size - line size
|
||||
```
|
||||
|
||||
**5. Memoization Leaks:**
|
||||
```ruby
|
||||
# ISSUE: Unbounded memoization cache
|
||||
def expensive_calculation(input)
|
||||
@cache ||= {}
|
||||
@cache[input] ||= perform_calculation(input)
|
||||
end
|
||||
|
||||
# OPTIMIZED: Use bounded cache (LRU)
|
||||
require 'lru_redux'
|
||||
|
||||
def expensive_calculation(input)
|
||||
@cache ||= LruRedux::Cache.new(1000)
|
||||
@cache.getset(input) { perform_calculation(input) }
|
||||
end
|
||||
|
||||
# Memory saved: Prevents cache from growing unbounded
|
||||
```
|
||||
|
||||
### Step 4: Readability and Idiom Analysis
|
||||
|
||||
**Detect Non-Idiomatic Code:**
|
||||
|
||||
**1. Conditional Assignment:**
|
||||
```ruby
|
||||
# NON-IDIOMATIC
|
||||
if user.name.nil?
|
||||
user.name = 'Guest'
|
||||
end
|
||||
|
||||
# IDIOMATIC
|
||||
user.name ||= 'Guest'
|
||||
|
||||
# NON-IDIOMATIC
|
||||
if value == nil
|
||||
value = default
|
||||
else
|
||||
value = value
|
||||
end
|
||||
|
||||
# IDIOMATIC
|
||||
value ||= default
|
||||
```
|
||||
|
||||
**2. Safe Navigation:**
|
||||
```ruby
|
||||
# NON-IDIOMATIC
|
||||
if user && user.profile && user.profile.avatar
|
||||
display(user.profile.avatar)
|
||||
end
|
||||
|
||||
# IDIOMATIC
|
||||
display(user&.profile&.avatar) if user&.profile&.avatar
|
||||
# or
|
||||
if avatar = user&.profile&.avatar
|
||||
display(avatar)
|
||||
end
|
||||
```
|
||||
|
||||
**3. Enumerable Methods:**
|
||||
```ruby
|
||||
# NON-IDIOMATIC
|
||||
found = nil
|
||||
users.each do |user|
|
||||
if user.active?
|
||||
found = user
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
# IDIOMATIC
|
||||
found = users.find(&:active?)
|
||||
|
||||
# NON-IDIOMATIC
|
||||
actives = []
|
||||
users.each do |user|
|
||||
actives << user if user.active?
|
||||
end
|
||||
|
||||
# IDIOMATIC
|
||||
actives = users.select(&:active?)
|
||||
|
||||
# NON-IDIOMATIC
|
||||
total = 0
|
||||
prices.each { |price| total += price }
|
||||
|
||||
# IDIOMATIC
|
||||
total = prices.sum
|
||||
# or
|
||||
total = prices.reduce(:+)
|
||||
```
|
||||
|
||||
**4. Guard Clauses:**
|
||||
```ruby
|
||||
# NON-IDIOMATIC
|
||||
def process(user)
|
||||
if user
|
||||
if user.active?
|
||||
if user.verified?
|
||||
# Main logic here
|
||||
perform_action(user)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# IDIOMATIC
|
||||
def process(user)
|
||||
return unless user
|
||||
return unless user.active?
|
||||
return unless user.verified?
|
||||
|
||||
perform_action(user)
|
||||
end
|
||||
```
|
||||
|
||||
**5. Pattern Matching (Ruby 3.0+):**
|
||||
```ruby
|
||||
# LESS IDIOMATIC (Ruby 3.0+)
|
||||
if response.is_a?(Hash) && response[:status] == 'success'
|
||||
handle_success(response[:data])
|
||||
elsif response.is_a?(Hash) && response[:status] == 'error'
|
||||
handle_error(response[:error])
|
||||
end
|
||||
|
||||
# MORE IDIOMATIC (Ruby 3.0+)
|
||||
case response
|
||||
in { status: 'success', data: }
|
||||
handle_success(data)
|
||||
in { status: 'error', error: }
|
||||
handle_error(error)
|
||||
end
|
||||
```
|
||||
|
||||
**6. Block Syntax:**
|
||||
```ruby
|
||||
# NON-IDIOMATIC: do/end for single line
|
||||
users.map do |u| u.name end
|
||||
|
||||
# IDIOMATIC: braces for single line
|
||||
users.map { |u| u.name }
|
||||
|
||||
# NON-IDIOMATIC: braces for multi-line
|
||||
users.select { |u|
|
||||
u.active? &&
|
||||
u.verified?
|
||||
}
|
||||
|
||||
# IDIOMATIC: do/end for multi-line
|
||||
users.select do |u|
|
||||
u.active? && u.verified?
|
||||
end
|
||||
```
|
||||
|
||||
**7. String Interpolation:**
|
||||
```ruby
|
||||
# NON-IDIOMATIC
|
||||
"Hello " + user.name + "!"
|
||||
|
||||
# IDIOMATIC
|
||||
"Hello #{user.name}!"
|
||||
|
||||
# NON-IDIOMATIC
|
||||
'Total: ' + total.to_s
|
||||
|
||||
# IDIOMATIC
|
||||
"Total: #{total}"
|
||||
```
|
||||
|
||||
### Step 5: Generate Benchmarks
|
||||
|
||||
**Create Benchmark Comparisons:**
|
||||
|
||||
```ruby
|
||||
# Generated benchmark file: benchmarks/optimization_comparison.rb
|
||||
require 'benchmark'
|
||||
|
||||
puts "Performance Comparison"
|
||||
puts "=" * 50
|
||||
|
||||
# Original implementation
|
||||
def original_method
|
||||
# Original code
|
||||
end
|
||||
|
||||
# Optimized implementation
|
||||
def optimized_method
|
||||
# Optimized code
|
||||
end
|
||||
|
||||
Benchmark.bm(20) do |x|
|
||||
x.report("Original:") do
|
||||
10_000.times { original_method }
|
||||
end
|
||||
|
||||
x.report("Optimized:") do
|
||||
10_000.times { optimized_method }
|
||||
end
|
||||
end
|
||||
|
||||
# Memory profiling
|
||||
require 'memory_profiler'
|
||||
|
||||
puts "\nMemory Comparison"
|
||||
puts "=" * 50
|
||||
|
||||
report = MemoryProfiler.report do
|
||||
original_method
|
||||
end
|
||||
|
||||
puts "Original Memory Usage:"
|
||||
puts " Total allocated: #{report.total_allocated_memsize} bytes"
|
||||
puts " Total retained: #{report.total_retained_memsize} bytes"
|
||||
|
||||
report = MemoryProfiler.report do
|
||||
optimized_method
|
||||
end
|
||||
|
||||
puts "\nOptimized Memory Usage:"
|
||||
puts " Total allocated: #{report.total_allocated_memsize} bytes"
|
||||
puts " Total retained: #{report.total_retained_memsize} bytes"
|
||||
```
|
||||
|
||||
### Step 6: Generate Optimization Report
|
||||
|
||||
**Comprehensive Report Structure:**
|
||||
|
||||
```
|
||||
================================================================================
|
||||
RUBY OPTIMIZATION REPORT
|
||||
================================================================================
|
||||
|
||||
File: app/services/data_processor.rb
|
||||
Focus: all
|
||||
Date: 2024-01-15
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
SUMMARY
|
||||
--------------------------------------------------------------------------------
|
||||
Total Issues Found: 18
|
||||
Performance: 8
|
||||
Memory: 5
|
||||
Readability: 5
|
||||
|
||||
Potential Improvements:
|
||||
Estimated Speed Gain: 2.5x faster
|
||||
Estimated Memory Reduction: 45%
|
||||
Code Quality: +15 readability score
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
PERFORMANCE OPTIMIZATIONS
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
1. Inefficient Enumeration (Line 23)
|
||||
Severity: Medium
|
||||
Impact: 20% speed improvement
|
||||
|
||||
Current:
|
||||
result = []
|
||||
users.each { |u| result << u.name.upcase }
|
||||
result
|
||||
|
||||
Optimized:
|
||||
users.map { |u| u.name.upcase }
|
||||
|
||||
Benchmark:
|
||||
Before: 1.45ms per 1000 items
|
||||
After: 1.15ms per 1000 items
|
||||
Improvement: 20.7% faster
|
||||
|
||||
2. N+1 Query Pattern (Line 45)
|
||||
Severity: High
|
||||
Impact: 10-100x speed improvement
|
||||
|
||||
Current:
|
||||
users.map { |u| { name: u.name, posts: u.posts.count } }
|
||||
|
||||
Optimized:
|
||||
users.eager(:posts).map { |u| { name: u.name, posts: u.posts.count } }
|
||||
|
||||
Benchmark:
|
||||
Before: 1250ms for 100 users with 10 posts each
|
||||
After: 25ms for 100 users with 10 posts each
|
||||
Improvement: 50x faster
|
||||
|
||||
[... more performance issues ...]
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
MEMORY OPTIMIZATIONS
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
1. String Allocation in Loop (Line 67)
|
||||
Severity: Medium
|
||||
Impact: 400 bytes saved per 1000 iterations
|
||||
|
||||
Current:
|
||||
1000.times { hash['key'] = value }
|
||||
|
||||
Optimized:
|
||||
1000.times { hash[:key] = value }
|
||||
|
||||
Memory:
|
||||
Before: 40KB allocated
|
||||
After: 160 bytes allocated
|
||||
Savings: 99.6%
|
||||
|
||||
[... more memory issues ...]
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
READABILITY IMPROVEMENTS
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
1. Non-Idiomatic Conditional (Line 89)
|
||||
Severity: Low
|
||||
Impact: Improved code clarity
|
||||
|
||||
Current:
|
||||
if user.name.nil?
|
||||
user.name = 'Guest'
|
||||
end
|
||||
|
||||
Idiomatic:
|
||||
user.name ||= 'Guest'
|
||||
|
||||
[... more readability issues ...]
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
COMPLEXITY METRICS
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Method Complexity (Flog scores):
|
||||
process_data: 45.2 (High - consider refactoring)
|
||||
transform_records: 23.1 (Medium)
|
||||
validate_input: 8.5 (Low)
|
||||
|
||||
Recommendations:
|
||||
- Extract methods from process_data to reduce complexity
|
||||
- Consider using service objects for complex operations
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
BENCHMARKS
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
File Generated: benchmarks/data_processor_comparison.rb
|
||||
|
||||
Run benchmarks:
|
||||
ruby benchmarks/data_processor_comparison.rb
|
||||
|
||||
Expected Results:
|
||||
Original: 2.450s
|
||||
Optimized: 0.980s
|
||||
Speedup: 2.5x
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
ACTION ITEMS
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
High Priority:
|
||||
1. Fix N+1 query in line 45 (50x performance gain)
|
||||
2. Optimize string building in line 67 (99% memory reduction)
|
||||
3. Refactor process_data method (complexity: 45.2)
|
||||
|
||||
Medium Priority:
|
||||
4. Use map instead of each+append (20% speed gain)
|
||||
5. Cache regex patterns (30% speed gain)
|
||||
6. Implement guard clauses in validate_input
|
||||
|
||||
Low Priority:
|
||||
7. Use idiomatic Ruby patterns throughout
|
||||
8. Apply consistent block syntax
|
||||
9. Improve variable naming
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
AUTOMATIC FIXES
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Low-risk changes that can be auto-applied:
|
||||
- String to symbol conversion (5 occurrences)
|
||||
- each to map conversion (3 occurrences)
|
||||
- Conditional to ||= conversion (4 occurrences)
|
||||
|
||||
Apply automatic fixes? [y/N]
|
||||
|
||||
================================================================================
|
||||
END REPORT
|
||||
================================================================================
|
||||
```
|
||||
|
||||
### Step 7: Optional - Apply Automatic Fixes
|
||||
|
||||
**Safe Transformations:**
|
||||
|
||||
For low-risk, well-defined improvements:
|
||||
|
||||
```ruby
|
||||
# Create optimized version of file
|
||||
# app/services/data_processor_optimized.rb
|
||||
|
||||
# Apply automatic transformations:
|
||||
# - String literals to symbols
|
||||
# - each+append to map
|
||||
# - if/nil? to ||=
|
||||
# - Block syntax corrections
|
||||
|
||||
# Generate diff
|
||||
# Show side-by-side comparison
|
||||
# Offer to replace original or keep both
|
||||
```
|
||||
|
||||
## Output Formats
|
||||
|
||||
### Console Output
|
||||
- Colored severity indicators (red/yellow/green)
|
||||
- Progress indicator during analysis
|
||||
- Summary statistics
|
||||
- Top issues highlighted
|
||||
|
||||
### Report Files
|
||||
- Detailed markdown report
|
||||
- Generated benchmark files
|
||||
- Optional optimized code files
|
||||
- Diff files for review
|
||||
|
||||
### JSON Output (Optional)
|
||||
```json
|
||||
{
|
||||
"file": "app/services/data_processor.rb",
|
||||
"summary": {
|
||||
"total_issues": 18,
|
||||
"performance": 8,
|
||||
"memory": 5,
|
||||
"readability": 5
|
||||
},
|
||||
"issues": [
|
||||
{
|
||||
"type": "performance",
|
||||
"severity": "high",
|
||||
"line": 45,
|
||||
"description": "N+1 query pattern",
|
||||
"impact": "50x speed improvement",
|
||||
"suggestion": "Use eager loading"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Handle invalid Ruby syntax gracefully
|
||||
- Skip non-Ruby files
|
||||
- Report files that cannot be parsed
|
||||
- Handle missing dependencies
|
||||
- Warn about risky optimizations
|
||||
- Preserve backups before modifications
|
||||
647
commands/sinatra-review.md
Normal file
647
commands/sinatra-review.md
Normal file
@@ -0,0 +1,647 @@
|
||||
---
|
||||
description: Review Sinatra code for security issues, performance problems, route conflicts, and framework best practices
|
||||
---
|
||||
|
||||
# Sinatra Review Command
|
||||
|
||||
Performs comprehensive code review of Sinatra applications, identifying security vulnerabilities, performance issues, routing conflicts, and deviations from best practices.
|
||||
|
||||
## Arguments
|
||||
|
||||
- **$1: path** (optional) - Path to review (defaults to current directory)
|
||||
|
||||
## Usage Examples
|
||||
|
||||
```bash
|
||||
# Review current directory
|
||||
/sinatra-review
|
||||
|
||||
# Review specific directory
|
||||
/sinatra-review /path/to/sinatra-app
|
||||
|
||||
# Review specific file
|
||||
/sinatra-review app/controllers/users_controller.rb
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Scan and Identify Application Files
|
||||
|
||||
**Discovery Phase:**
|
||||
1. Locate `config.ru` to identify Rack application
|
||||
2. Find Sinatra application files (controllers, routes)
|
||||
3. Identify application structure (classic vs modular)
|
||||
4. Scan for middleware configuration
|
||||
5. Locate view templates and helpers
|
||||
6. Find configuration files
|
||||
7. Identify database and model files
|
||||
|
||||
**File Patterns to Search:**
|
||||
```bash
|
||||
# Application files
|
||||
*.rb files inheriting from Sinatra::Base
|
||||
config.ru
|
||||
app.rb (classic style)
|
||||
app/controllers/*.rb
|
||||
lib/**/*.rb
|
||||
|
||||
# View templates
|
||||
views/**/*.erb
|
||||
views/**/*.haml
|
||||
views/**/*.slim
|
||||
|
||||
# Configuration
|
||||
config/*.rb
|
||||
Gemfile
|
||||
.env files
|
||||
```
|
||||
|
||||
### Step 2: Analyze Route Definitions
|
||||
|
||||
**Route Conflict Detection:**
|
||||
|
||||
Check for:
|
||||
1. **Duplicate routes** with same path and HTTP method
|
||||
2. **Overlapping routes** where order matters (specific before generic)
|
||||
3. **Missing route constraints** leading to ambiguous matching
|
||||
4. **Wildcard route conflicts**
|
||||
|
||||
**Examples of Issues:**
|
||||
|
||||
```ruby
|
||||
# ISSUE: Route order conflict
|
||||
get '/users/new' do
|
||||
# Never reached because of wildcard below
|
||||
end
|
||||
|
||||
get '/users/:id' do
|
||||
# This catches /users/new
|
||||
end
|
||||
|
||||
# FIX: Specific routes before wildcards
|
||||
get '/users/new' do
|
||||
# Now reached first
|
||||
end
|
||||
|
||||
get '/users/:id' do
|
||||
# Only catches other IDs
|
||||
end
|
||||
|
||||
# ISSUE: Duplicate routes
|
||||
get '/api/users' do
|
||||
# First definition
|
||||
end
|
||||
|
||||
get '/api/users' do
|
||||
# Overwrites first - only this runs
|
||||
end
|
||||
|
||||
# ISSUE: Missing validation
|
||||
get '/users/:id' do
|
||||
user = User.find(params[:id]) # What if id is not numeric?
|
||||
end
|
||||
|
||||
# FIX: Add validation
|
||||
get '/users/:id', id: /\d+/ do
|
||||
user = User.find(params[:id])
|
||||
end
|
||||
```
|
||||
|
||||
**Route Analysis Report:**
|
||||
```
|
||||
Route Analysis:
|
||||
Total routes: 25
|
||||
GET: 15, POST: 5, PUT: 3, DELETE: 2
|
||||
|
||||
⚠ Warnings:
|
||||
- Route order issue in app/controllers/users_controller.rb:15
|
||||
GET /users/:id should be after GET /users/new
|
||||
|
||||
- Missing parameter validation in app/controllers/posts_controller.rb:32
|
||||
Route GET /posts/:id should validate :id is numeric
|
||||
```
|
||||
|
||||
### Step 3: Security Analysis
|
||||
|
||||
**Security Checklist:**
|
||||
|
||||
**1. CSRF Protection:**
|
||||
```ruby
|
||||
# CHECK: Is CSRF protection enabled?
|
||||
use Rack::Protection
|
||||
# or
|
||||
use Rack::Protection::AuthenticityToken
|
||||
|
||||
# ISSUE: Missing CSRF for POST/PUT/DELETE
|
||||
post '/users' do
|
||||
User.create(params[:user]) # Vulnerable to CSRF
|
||||
end
|
||||
|
||||
# FIX: Ensure Rack::Protection is enabled
|
||||
```
|
||||
|
||||
**2. XSS Prevention:**
|
||||
```ruby
|
||||
# CHECK: Are templates auto-escaping HTML?
|
||||
# ERB: Use <%= %> (escapes) not <%== %> (raw)
|
||||
|
||||
# ISSUE: Raw user input in template
|
||||
<div><%== @user.bio %></div>
|
||||
|
||||
# FIX: Escape user input
|
||||
<div><%= @user.bio %></div>
|
||||
|
||||
# CHECK: JSON responses properly encoded
|
||||
# ISSUE: Manual JSON creation
|
||||
get '/api/users' do
|
||||
"{ \"name\": \"#{user.name}\" }" # XSS if name contains quotes
|
||||
end
|
||||
|
||||
# FIX: Use JSON library
|
||||
get '/api/users' do
|
||||
json({ name: user.name })
|
||||
end
|
||||
```
|
||||
|
||||
**3. SQL Injection:**
|
||||
```ruby
|
||||
# ISSUE: String interpolation in queries
|
||||
DB["SELECT * FROM users WHERE email = '#{params[:email]}'"]
|
||||
|
||||
# FIX: Use parameterized queries
|
||||
DB["SELECT * FROM users WHERE email = ?", params[:email]]
|
||||
|
||||
# ISSUE: Unsafe ActiveRecord
|
||||
User.where("email = '#{params[:email]}'")
|
||||
|
||||
# FIX: Use hash conditions
|
||||
User.where(email: params[:email])
|
||||
```
|
||||
|
||||
**4. Authentication & Authorization:**
|
||||
```ruby
|
||||
# CHECK: Protected routes have authentication
|
||||
# ISSUE: Admin route without auth check
|
||||
delete '/users/:id' do
|
||||
User.find(params[:id]).destroy # No auth check!
|
||||
end
|
||||
|
||||
# FIX: Add authentication
|
||||
before '/admin/*' do
|
||||
halt 401 unless current_user&.admin?
|
||||
end
|
||||
|
||||
# CHECK: Session security
|
||||
# ISSUE: Weak session configuration
|
||||
use Rack::Session::Cookie, secret: 'easy'
|
||||
|
||||
# FIX: Strong secret and secure flags
|
||||
use Rack::Session::Cookie,
|
||||
secret: ENV['SESSION_SECRET'], # Long random string
|
||||
same_site: :strict,
|
||||
httponly: true,
|
||||
secure: production?
|
||||
```
|
||||
|
||||
**5. Mass Assignment:**
|
||||
```ruby
|
||||
# ISSUE: Accepting all params
|
||||
User.create(params)
|
||||
|
||||
# FIX: Whitelist allowed attributes
|
||||
def user_params
|
||||
params.slice(:name, :email, :bio)
|
||||
end
|
||||
|
||||
User.create(user_params)
|
||||
```
|
||||
|
||||
**6. File Upload Security:**
|
||||
```ruby
|
||||
# ISSUE: Unrestricted file uploads
|
||||
post '/upload' do
|
||||
File.write("uploads/#{params[:file][:filename]}", params[:file][:tempfile].read)
|
||||
end
|
||||
|
||||
# FIX: Validate file type and sanitize filename
|
||||
post '/upload' do
|
||||
file = params[:file]
|
||||
|
||||
# Validate content type
|
||||
halt 400 unless ['image/jpeg', 'image/png'].include?(file[:type])
|
||||
|
||||
# Sanitize filename
|
||||
filename = File.basename(file[:filename]).gsub(/[^a-zA-Z0-9\._-]/, '')
|
||||
|
||||
# Save with random name
|
||||
secure_name = "#{SecureRandom.hex}-#{filename}"
|
||||
File.write("uploads/#{secure_name}", file[:tempfile].read)
|
||||
end
|
||||
```
|
||||
|
||||
**7. Information Disclosure:**
|
||||
```ruby
|
||||
# ISSUE: Detailed error messages in production
|
||||
configure :production do
|
||||
set :show_exceptions, true # Exposes stack traces
|
||||
end
|
||||
|
||||
# FIX: Hide errors in production
|
||||
configure :production do
|
||||
set :show_exceptions, false
|
||||
set :dump_errors, false
|
||||
end
|
||||
|
||||
error do
|
||||
log_error(env['sinatra.error'])
|
||||
json({ error: 'Internal server error' }, 500)
|
||||
end
|
||||
```
|
||||
|
||||
**Security Report:**
|
||||
```
|
||||
Security Analysis:
|
||||
✓ CSRF protection enabled (Rack::Protection)
|
||||
✓ Session configured securely
|
||||
⚠ Potential Issues:
|
||||
- SQL injection risk in app/models/user.rb:45
|
||||
- Raw HTML output in views/profile.erb:12
|
||||
- Missing authentication check in app/controllers/admin_controller.rb:23
|
||||
- Weak session secret detected
|
||||
|
||||
Critical: 1
|
||||
High: 2
|
||||
Medium: 3
|
||||
Low: 2
|
||||
```
|
||||
|
||||
### Step 4: Review Middleware Configuration
|
||||
|
||||
**Middleware Analysis:**
|
||||
|
||||
Check for:
|
||||
1. **Missing essential middleware** (Protection, CommonLogger)
|
||||
2. **Incorrect ordering** (e.g., session after auth)
|
||||
3. **Performance issues** (e.g., no compression)
|
||||
4. **Security middleware** properly configured
|
||||
|
||||
**Common Issues:**
|
||||
|
||||
```ruby
|
||||
# ISSUE: Missing compression
|
||||
# FIX: Add Rack::Deflater
|
||||
use Rack::Deflater
|
||||
|
||||
# ISSUE: Session middleware after authentication
|
||||
use TokenAuth
|
||||
use Rack::Session::Cookie # Session needed by auth!
|
||||
|
||||
# FIX: Session before authentication
|
||||
use Rack::Session::Cookie
|
||||
use TokenAuth
|
||||
|
||||
# ISSUE: No security headers
|
||||
# FIX: Add Rack::Protection
|
||||
use Rack::Protection, except: [:session_hijacking]
|
||||
|
||||
# ISSUE: Static file serving after application
|
||||
run MyApp
|
||||
use Rack::Static # Never reached!
|
||||
|
||||
# FIX: Static before application
|
||||
use Rack::Static, urls: ['/css', '/js'], root: 'public'
|
||||
run MyApp
|
||||
```
|
||||
|
||||
**Middleware Report:**
|
||||
```
|
||||
Middleware Configuration:
|
||||
✓ Rack::CommonLogger (logging)
|
||||
✓ Rack::Session::Cookie (sessions)
|
||||
✓ Rack::Protection (security)
|
||||
⚠ Warnings:
|
||||
- Missing Rack::Deflater (compression)
|
||||
- Middleware order issue: Session should be before CustomAuth
|
||||
- Consider adding Rack::Attack for rate limiting
|
||||
```
|
||||
|
||||
### Step 5: Performance Assessment
|
||||
|
||||
**Performance Patterns to Check:**
|
||||
|
||||
**1. Database Query Optimization:**
|
||||
```ruby
|
||||
# ISSUE: N+1 queries
|
||||
get '/users' do
|
||||
users = User.all
|
||||
users.map { |u| { name: u.name, posts: u.posts.count } }
|
||||
# Queries DB for each user's posts
|
||||
end
|
||||
|
||||
# FIX: Eager load or use counter cache
|
||||
get '/users' do
|
||||
users = User.eager(:posts).all
|
||||
users.map { |u| { name: u.name, posts: u.posts.count } }
|
||||
end
|
||||
|
||||
# ISSUE: Loading entire collection
|
||||
get '/users' do
|
||||
json User.all.map(&:to_hash) # Load all users in memory
|
||||
end
|
||||
|
||||
# FIX: Paginate
|
||||
get '/users' do
|
||||
page = params[:page]&.to_i || 1
|
||||
per_page = 50
|
||||
|
||||
users = User.limit(per_page).offset((page - 1) * per_page)
|
||||
json users.map(&:to_hash)
|
||||
end
|
||||
```
|
||||
|
||||
**2. Caching Opportunities:**
|
||||
```ruby
|
||||
# ISSUE: Expensive operation on every request
|
||||
get '/stats' do
|
||||
json calculate_expensive_stats # Takes 2 seconds
|
||||
end
|
||||
|
||||
# FIX: Add caching
|
||||
get '/stats' do
|
||||
stats = cache.fetch('stats', expires_in: 300) do
|
||||
calculate_expensive_stats
|
||||
end
|
||||
json stats
|
||||
end
|
||||
|
||||
# ISSUE: No HTTP caching headers
|
||||
get '/public/data' do
|
||||
json PublicData.all
|
||||
end
|
||||
|
||||
# FIX: Add cache control
|
||||
get '/public/data' do
|
||||
cache_control :public, max_age: 3600
|
||||
json PublicData.all
|
||||
end
|
||||
```
|
||||
|
||||
**3. Response Optimization:**
|
||||
```ruby
|
||||
# ISSUE: Rendering large response synchronously
|
||||
get '/large-export' do
|
||||
csv = generate_large_csv # Blocks for 30 seconds
|
||||
send_file csv
|
||||
end
|
||||
|
||||
# FIX: Stream or queue as background job
|
||||
get '/large-export' do
|
||||
stream do |out|
|
||||
CSV.generate(out) do |csv|
|
||||
User.find_each do |user|
|
||||
csv << user.to_csv_row
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Performance Report:**
|
||||
```
|
||||
Performance Analysis:
|
||||
⚠ Issues Detected:
|
||||
- Potential N+1 query in app/controllers/users_controller.rb:42
|
||||
- Missing pagination in GET /api/posts (returns all records)
|
||||
- No caching headers on GET /api/public/data
|
||||
- Expensive operation in GET /stats without caching
|
||||
|
||||
Recommendations:
|
||||
- Add database query optimization (eager loading)
|
||||
- Implement pagination for collection endpoints
|
||||
- Add HTTP caching headers for static content
|
||||
- Consider Redis caching for expensive operations
|
||||
```
|
||||
|
||||
### Step 6: Error Handling Review
|
||||
|
||||
**Error Handling Patterns:**
|
||||
|
||||
```ruby
|
||||
# ISSUE: No error handlers defined
|
||||
get '/users/:id' do
|
||||
User.find(params[:id]) # Raises if not found, shows stack trace
|
||||
end
|
||||
|
||||
# FIX: Add error handlers
|
||||
error ActiveRecord::RecordNotFound do
|
||||
json({ error: 'Not found' }, 404)
|
||||
end
|
||||
|
||||
error 404 do
|
||||
json({ error: 'Endpoint not found' }, 404)
|
||||
end
|
||||
|
||||
error 500 do
|
||||
json({ error: 'Internal server error' }, 500)
|
||||
end
|
||||
|
||||
# ISSUE: Not handling exceptions in routes
|
||||
post '/users' do
|
||||
User.create!(params) # Raises on validation error
|
||||
end
|
||||
|
||||
# FIX: Handle exceptions
|
||||
post '/users' do
|
||||
user = User.create(params)
|
||||
if user.persisted?
|
||||
json(user.to_hash, 201)
|
||||
else
|
||||
json({ errors: user.errors }, 422)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Step 7: Testing Coverage
|
||||
|
||||
**Test Analysis:**
|
||||
|
||||
Check for:
|
||||
1. Test files exist
|
||||
2. Route coverage
|
||||
3. Error case testing
|
||||
4. Integration vs unit tests
|
||||
5. Test quality and patterns
|
||||
|
||||
**Report:**
|
||||
```
|
||||
Testing Analysis:
|
||||
Framework: RSpec
|
||||
Total specs: 45
|
||||
Coverage: 78%
|
||||
|
||||
⚠ Missing Tests:
|
||||
- No tests for POST /api/users
|
||||
- Error cases not tested in app/controllers/posts_controller.rb
|
||||
- Missing integration tests for authentication flow
|
||||
|
||||
Recommendations:
|
||||
- Add tests for all POST/PUT/DELETE routes
|
||||
- Test error scenarios (404, 422, 500)
|
||||
- Increase coverage to 90%+
|
||||
```
|
||||
|
||||
### Step 8: Generate Comprehensive Report
|
||||
|
||||
**Final Report Structure:**
|
||||
|
||||
```
|
||||
================================================================================
|
||||
SINATRA CODE REVIEW REPORT
|
||||
================================================================================
|
||||
|
||||
Project: my-sinatra-app
|
||||
Path: /path/to/app
|
||||
Date: 2024-01-15
|
||||
Reviewer: Sinatra Review Tool
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
SUMMARY
|
||||
--------------------------------------------------------------------------------
|
||||
Total Issues: 15
|
||||
Critical: 2
|
||||
High: 4
|
||||
Medium: 6
|
||||
Low: 3
|
||||
|
||||
Categories:
|
||||
Security: 5 issues
|
||||
Performance: 4 issues
|
||||
Best Practices: 6 issues
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
CRITICAL ISSUES
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
1. SQL Injection Vulnerability
|
||||
Location: app/models/user.rb:45
|
||||
Severity: Critical
|
||||
|
||||
Issue:
|
||||
DB["SELECT * FROM users WHERE email = '#{email}'"]
|
||||
|
||||
Fix:
|
||||
DB["SELECT * FROM users WHERE email = ?", email]
|
||||
|
||||
Impact: Attacker can execute arbitrary SQL queries
|
||||
|
||||
2. Missing Authentication on Admin Route
|
||||
Location: app/controllers/admin_controller.rb:23
|
||||
Severity: Critical
|
||||
|
||||
Issue:
|
||||
delete '/users/:id' do
|
||||
User.find(params[:id]).destroy
|
||||
end
|
||||
|
||||
Fix:
|
||||
before '/admin/*' do
|
||||
authenticate_admin!
|
||||
end
|
||||
|
||||
Impact: Unauthorized users can delete records
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
HIGH PRIORITY ISSUES
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
[List high priority issues...]
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
RECOMMENDATIONS
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Security:
|
||||
- Enable Rack::Protection::AuthenticityToken for CSRF
|
||||
- Rotate session secret to strong random value
|
||||
- Implement rate limiting with Rack::Attack
|
||||
- Add Content-Security-Policy headers
|
||||
|
||||
Performance:
|
||||
- Add Rack::Deflater for response compression
|
||||
- Implement caching strategy (Redis or Memcached)
|
||||
- Add pagination to collection endpoints
|
||||
- Optimize database queries (N+1 issues)
|
||||
|
||||
Testing:
|
||||
- Increase test coverage to 90%+
|
||||
- Add integration tests for critical flows
|
||||
- Test error scenarios
|
||||
- Add security-focused tests
|
||||
|
||||
Best Practices:
|
||||
- Extract business logic to service objects
|
||||
- Use helpers for repeated code
|
||||
- Implement proper error handling
|
||||
- Add API documentation
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
DETAILED FINDINGS
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
[Full list of all issues with locations, descriptions, and fixes]
|
||||
|
||||
================================================================================
|
||||
END REPORT
|
||||
================================================================================
|
||||
```
|
||||
|
||||
## Review Categories
|
||||
|
||||
### Security
|
||||
- CSRF protection
|
||||
- XSS prevention
|
||||
- SQL injection
|
||||
- Authentication/Authorization
|
||||
- Session security
|
||||
- Mass assignment
|
||||
- File upload security
|
||||
- Information disclosure
|
||||
- Secure headers
|
||||
|
||||
### Performance
|
||||
- Database query optimization
|
||||
- N+1 queries
|
||||
- Caching opportunities
|
||||
- Response optimization
|
||||
- Static asset handling
|
||||
- Connection pooling
|
||||
|
||||
### Best Practices
|
||||
- Route organization
|
||||
- Error handling
|
||||
- Code organization
|
||||
- Helper usage
|
||||
- Configuration management
|
||||
- Logging
|
||||
- Documentation
|
||||
|
||||
### Testing
|
||||
- Test coverage
|
||||
- Test quality
|
||||
- Missing tests
|
||||
- Test organization
|
||||
|
||||
## Output Format
|
||||
|
||||
- Console output with colored severity indicators
|
||||
- Detailed report with file locations and line numbers
|
||||
- Suggested fixes with code examples
|
||||
- Priority-sorted issue list
|
||||
- Summary statistics
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Handle non-Sinatra Ruby applications gracefully
|
||||
- Report when application structure cannot be determined
|
||||
- Skip non-readable files
|
||||
- Handle parse errors in Ruby files
|
||||
654
commands/sinatra-scaffold.md
Normal file
654
commands/sinatra-scaffold.md
Normal file
@@ -0,0 +1,654 @@
|
||||
---
|
||||
description: Scaffold new Sinatra applications with modern structure, best practices, testing setup, and deployment configuration
|
||||
---
|
||||
|
||||
# Sinatra Scaffold Command
|
||||
|
||||
Scaffolds a new Sinatra application with modern project structure, testing framework, and deployment configuration.
|
||||
|
||||
## Arguments
|
||||
|
||||
- **$1: project-name** (required) - Name of the project/application
|
||||
- **$2: type** (optional) - Application type: `classic`, `modular`, or `api` (default: `modular`)
|
||||
- **$3: options** (optional) - JSON string with configuration options:
|
||||
- `testing`: `rspec` or `minitest` (default: `rspec`)
|
||||
- `database`: `sequel`, `activerecord`, or `none` (default: `sequel`)
|
||||
- `frontend`: `none`, `erb`, or `haml` (default: `erb`)
|
||||
|
||||
## Usage Examples
|
||||
|
||||
```bash
|
||||
# Basic modular app with defaults
|
||||
/sinatra-scaffold my-app
|
||||
|
||||
# Classic app with RSpec and no database
|
||||
/sinatra-scaffold simple-app classic '{"testing":"rspec","database":"none","frontend":"erb"}'
|
||||
|
||||
# API-only app with Minitest and ActiveRecord
|
||||
/sinatra-scaffold api-service api '{"testing":"minitest","database":"activerecord","frontend":"none"}'
|
||||
|
||||
# Full-featured modular app
|
||||
/sinatra-scaffold webapp modular '{"testing":"rspec","database":"sequel","frontend":"haml"}'
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Validate and Initialize
|
||||
|
||||
**Actions:**
|
||||
1. Validate project name format (alphanumeric, hyphens, underscores)
|
||||
2. Check if directory already exists
|
||||
3. Parse and validate options JSON
|
||||
4. Create project directory structure
|
||||
|
||||
**Validation:**
|
||||
```bash
|
||||
# Check project name
|
||||
if [[ ! "$PROJECT_NAME" =~ ^[a-zA-Z0-9_-]+$ ]]; then
|
||||
echo "Error: Invalid project name. Use alphanumeric characters, hyphens, or underscores."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if directory exists
|
||||
if [ -d "$PROJECT_NAME" ]; then
|
||||
echo "Error: Directory '$PROJECT_NAME' already exists."
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
### Step 2: Create Directory Structure
|
||||
|
||||
**Classic Structure:**
|
||||
```
|
||||
project-name/
|
||||
├── app.rb
|
||||
├── config.ru
|
||||
├── Gemfile
|
||||
├── Rakefile
|
||||
├── config/
|
||||
│ └── environment.rb
|
||||
├── public/
|
||||
│ ├── css/
|
||||
│ ├── js/
|
||||
│ └── images/
|
||||
├── views/
|
||||
│ ├── layout.erb
|
||||
│ └── index.erb
|
||||
├── spec/ or test/
|
||||
└── README.md
|
||||
```
|
||||
|
||||
**Modular Structure:**
|
||||
```
|
||||
project-name/
|
||||
├── app/
|
||||
│ ├── controllers/
|
||||
│ │ ├── application_controller.rb
|
||||
│ │ └── base_controller.rb
|
||||
│ ├── models/
|
||||
│ ├── services/
|
||||
│ └── helpers/
|
||||
├── config/
|
||||
│ ├── environment.rb
|
||||
│ ├── database.yml (if database selected)
|
||||
│ └── puma.rb
|
||||
├── config.ru
|
||||
├── db/
|
||||
│ └── migrations/
|
||||
├── lib/
|
||||
│ └── tasks/
|
||||
├── public/
|
||||
│ ├── css/
|
||||
│ ├── js/
|
||||
│ └── images/
|
||||
├── views/
|
||||
│ ├── layout.erb
|
||||
│ └── index.erb
|
||||
├── spec/ or test/
|
||||
│ ├── spec_helper.rb
|
||||
│ └── controllers/
|
||||
├── Gemfile
|
||||
├── Rakefile
|
||||
├── .env.example
|
||||
├── .gitignore
|
||||
└── README.md
|
||||
```
|
||||
|
||||
**API Structure:**
|
||||
```
|
||||
project-name/
|
||||
├── app/
|
||||
│ ├── controllers/
|
||||
│ │ ├── api_controller.rb
|
||||
│ │ └── base_controller.rb
|
||||
│ ├── models/
|
||||
│ ├── services/
|
||||
│ └── serializers/
|
||||
├── config/
|
||||
│ ├── environment.rb
|
||||
│ ├── database.yml
|
||||
│ └── puma.rb
|
||||
├── config.ru
|
||||
├── db/
|
||||
│ └── migrations/
|
||||
├── lib/
|
||||
├── spec/ or test/
|
||||
│ ├── spec_helper.rb
|
||||
│ ├── requests/
|
||||
│ └── support/
|
||||
├── Gemfile
|
||||
├── Rakefile
|
||||
├── .env.example
|
||||
├── .gitignore
|
||||
└── README.md
|
||||
```
|
||||
|
||||
### Step 3: Generate Gemfile
|
||||
|
||||
**Base Dependencies (All Types):**
|
||||
```ruby
|
||||
source 'https://rubygems.org'
|
||||
|
||||
ruby '~> 3.2'
|
||||
|
||||
gem 'sinatra', '~> 3.0'
|
||||
gem 'sinatra-contrib', '~> 3.0'
|
||||
gem 'puma', '~> 6.0'
|
||||
gem 'rake', '~> 13.0'
|
||||
gem 'dotenv', '~> 2.8'
|
||||
|
||||
# Add database gems if selected
|
||||
# gem 'sequel', '~> 5.0' or gem 'activerecord', '~> 7.0'
|
||||
# gem 'pg', '~> 1.5' # PostgreSQL
|
||||
|
||||
# Add frontend gems if not API
|
||||
# gem 'haml', '~> 6.0' if haml selected
|
||||
|
||||
group :development, :test do
|
||||
gem 'rspec', '~> 3.12' # or minitest
|
||||
gem 'rack-test', '~> 2.0'
|
||||
gem 'rerun', '~> 0.14'
|
||||
end
|
||||
|
||||
group :development do
|
||||
gem 'pry', '~> 0.14'
|
||||
end
|
||||
|
||||
group :test do
|
||||
gem 'simplecov', '~> 0.22', require: false
|
||||
gem 'database_cleaner-sequel', '~> 2.0' # if using Sequel
|
||||
end
|
||||
```
|
||||
|
||||
**Additional Dependencies by Type:**
|
||||
|
||||
For modular/API:
|
||||
```ruby
|
||||
gem 'rack-cors', '~> 2.0' # For API
|
||||
gem 'multi_json', '~> 1.15'
|
||||
```
|
||||
|
||||
For database options:
|
||||
```ruby
|
||||
# Sequel
|
||||
gem 'sequel', '~> 5.0'
|
||||
gem 'pg', '~> 1.5'
|
||||
|
||||
# ActiveRecord
|
||||
gem 'activerecord', '~> 7.0'
|
||||
gem 'pg', '~> 1.5'
|
||||
gem 'sinatra-activerecord', '~> 2.0'
|
||||
```
|
||||
|
||||
### Step 4: Generate Application Files
|
||||
|
||||
**Classic App (app.rb):**
|
||||
```ruby
|
||||
require 'sinatra'
|
||||
require 'sinatra/reloader' if development?
|
||||
require_relative 'config/environment'
|
||||
|
||||
get '/' do
|
||||
erb :index
|
||||
end
|
||||
```
|
||||
|
||||
**Modular Base Controller (app/controllers/base_controller.rb):**
|
||||
```ruby
|
||||
require 'sinatra/base'
|
||||
require 'sinatra/json'
|
||||
|
||||
class BaseController < Sinatra::Base
|
||||
configure do
|
||||
set :root, File.expand_path('../..', __dir__)
|
||||
set :views, Proc.new { File.join(root, 'views') }
|
||||
set :public_folder, Proc.new { File.join(root, 'public') }
|
||||
set :show_exceptions, false
|
||||
set :raise_errors, false
|
||||
end
|
||||
|
||||
configure :development do
|
||||
require 'sinatra/reloader'
|
||||
register Sinatra::Reloader
|
||||
end
|
||||
|
||||
helpers do
|
||||
def json_response(data, status = 200)
|
||||
halt status, { 'Content-Type' => 'application/json' }, data.to_json
|
||||
end
|
||||
end
|
||||
|
||||
error do
|
||||
error = env['sinatra.error']
|
||||
status 500
|
||||
json_response({ error: error.message })
|
||||
end
|
||||
|
||||
not_found do
|
||||
json_response({ error: 'Not found' }, 404)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Application Controller (app/controllers/application_controller.rb):**
|
||||
```ruby
|
||||
require_relative 'base_controller'
|
||||
|
||||
class ApplicationController < BaseController
|
||||
get '/' do
|
||||
erb :index
|
||||
end
|
||||
|
||||
get '/health' do
|
||||
json_response({ status: 'ok', timestamp: Time.now.to_i })
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**API Controller (for API type):**
|
||||
```ruby
|
||||
require_relative 'base_controller'
|
||||
|
||||
class ApiController < BaseController
|
||||
before do
|
||||
content_type :json
|
||||
end
|
||||
|
||||
# CORS for development
|
||||
configure :development do
|
||||
before do
|
||||
headers 'Access-Control-Allow-Origin' => '*'
|
||||
end
|
||||
|
||||
options '*' do
|
||||
headers 'Access-Control-Allow-Methods' => 'GET, POST, PUT, PATCH, DELETE, OPTIONS'
|
||||
headers 'Access-Control-Allow-Headers' => 'Content-Type, Authorization'
|
||||
200
|
||||
end
|
||||
end
|
||||
|
||||
get '/' do
|
||||
json_response({
|
||||
name: 'API',
|
||||
version: '1.0',
|
||||
endpoints: [
|
||||
{ path: '/health', method: 'GET' }
|
||||
]
|
||||
})
|
||||
end
|
||||
|
||||
get '/health' do
|
||||
json_response({ status: 'healthy', timestamp: Time.now.to_i })
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Step 5: Create Configuration Files
|
||||
|
||||
**config.ru:**
|
||||
```ruby
|
||||
require_relative 'config/environment'
|
||||
|
||||
# Modular
|
||||
map '/' do
|
||||
run ApplicationController
|
||||
end
|
||||
|
||||
# API
|
||||
# map '/api/v1' do
|
||||
# run ApiController
|
||||
# end
|
||||
```
|
||||
|
||||
**config/environment.rb:**
|
||||
```ruby
|
||||
ENV['RACK_ENV'] ||= 'development'
|
||||
|
||||
require 'bundler'
|
||||
Bundler.require(:default, ENV['RACK_ENV'])
|
||||
|
||||
# Load environment variables
|
||||
require 'dotenv'
|
||||
Dotenv.load(".env.#{ENV['RACK_ENV']}", '.env')
|
||||
|
||||
# Database setup (if selected)
|
||||
# require_relative 'database'
|
||||
|
||||
# Load application files
|
||||
Dir[File.join(__dir__, '../app/**/*.rb')].sort.each { |file| require file }
|
||||
```
|
||||
|
||||
**config/database.yml (if database selected):**
|
||||
```yaml
|
||||
default: &default
|
||||
adapter: postgresql
|
||||
encoding: unicode
|
||||
pool: <%= ENV.fetch("DB_POOL", 5) %>
|
||||
host: <%= ENV.fetch("DB_HOST", "localhost") %>
|
||||
|
||||
development:
|
||||
<<: *default
|
||||
database: <%= ENV.fetch("PROJECT_NAME") %>_development
|
||||
|
||||
test:
|
||||
<<: *default
|
||||
database: <%= ENV.fetch("PROJECT_NAME") %>_test
|
||||
|
||||
production:
|
||||
<<: *default
|
||||
database: <%= ENV.fetch("DB_NAME") %>
|
||||
username: <%= ENV.fetch("DB_USER") %>
|
||||
password: <%= ENV.fetch("DB_PASSWORD") %>
|
||||
```
|
||||
|
||||
**config/puma.rb:**
|
||||
```ruby
|
||||
workers ENV.fetch('WEB_CONCURRENCY', 2)
|
||||
threads_count = ENV.fetch('MAX_THREADS', 5)
|
||||
threads threads_count, threads_count
|
||||
|
||||
preload_app!
|
||||
|
||||
port ENV.fetch('PORT', 3000)
|
||||
environment ENV.fetch('RACK_ENV', 'development')
|
||||
|
||||
on_worker_boot do
|
||||
# Database reconnection if using ActiveRecord
|
||||
# ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
|
||||
end
|
||||
```
|
||||
|
||||
### Step 6: Set Up Testing Framework
|
||||
|
||||
**RSpec spec/spec_helper.rb:**
|
||||
```ruby
|
||||
ENV['RACK_ENV'] = 'test'
|
||||
|
||||
require 'simplecov'
|
||||
SimpleCov.start
|
||||
|
||||
require_relative '../config/environment'
|
||||
require 'rack/test'
|
||||
require 'rspec'
|
||||
|
||||
# Database cleaner setup (if database)
|
||||
# require 'database_cleaner/sequel'
|
||||
|
||||
RSpec.configure do |config|
|
||||
config.include Rack::Test::Methods
|
||||
|
||||
config.expect_with :rspec do |expectations|
|
||||
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
||||
end
|
||||
|
||||
config.mock_with :rspec do |mocks|
|
||||
mocks.verify_partial_doubles = true
|
||||
end
|
||||
|
||||
config.shared_context_metadata_behavior = :apply_to_host_groups
|
||||
|
||||
# Database cleaner (if database)
|
||||
# config.before(:suite) do
|
||||
# DatabaseCleaner.strategy = :transaction
|
||||
# DatabaseCleaner.clean_with(:truncation)
|
||||
# end
|
||||
#
|
||||
# config.around(:each) do |example|
|
||||
# DatabaseCleaner.cleaning do
|
||||
# example.run
|
||||
# end
|
||||
# end
|
||||
end
|
||||
```
|
||||
|
||||
**Example spec/controllers/application_controller_spec.rb:**
|
||||
```ruby
|
||||
require_relative '../spec_helper'
|
||||
|
||||
RSpec.describe ApplicationController do
|
||||
def app
|
||||
ApplicationController
|
||||
end
|
||||
|
||||
describe 'GET /' do
|
||||
it 'returns success' do
|
||||
get '/'
|
||||
expect(last_response).to be_ok
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /health' do
|
||||
it 'returns health status' do
|
||||
get '/health'
|
||||
expect(last_response).to be_ok
|
||||
json = JSON.parse(last_response.body)
|
||||
expect(json['status']).to eq('ok')
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Step 7: Create Supporting Files
|
||||
|
||||
**.env.example:**
|
||||
```bash
|
||||
RACK_ENV=development
|
||||
PORT=3000
|
||||
|
||||
# Database (if selected)
|
||||
DB_HOST=localhost
|
||||
DB_NAME=project_name_development
|
||||
DB_USER=postgres
|
||||
DB_PASSWORD=
|
||||
|
||||
# Session
|
||||
SESSION_SECRET=your-secret-key-here
|
||||
|
||||
# External services
|
||||
# API_KEY=
|
||||
```
|
||||
|
||||
**.gitignore:**
|
||||
```
|
||||
*.gem
|
||||
*.rbc
|
||||
/.config
|
||||
/coverage/
|
||||
/InstalledFiles
|
||||
/pkg/
|
||||
/spec/reports/
|
||||
/spec/examples.txt
|
||||
/test/tmp/
|
||||
/test/version_tmp/
|
||||
/tmp/
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
|
||||
# Database
|
||||
*.sqlite3
|
||||
*.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Editor files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
```
|
||||
|
||||
**Rakefile:**
|
||||
```ruby
|
||||
require_relative 'config/environment'
|
||||
|
||||
# Database tasks (if using Sequel)
|
||||
if defined?(Sequel)
|
||||
require 'sequel/core'
|
||||
namespace :db do
|
||||
desc 'Run migrations'
|
||||
task :migrate, [:version] do |t, args|
|
||||
Sequel.extension :migration
|
||||
db = Sequel.connect(ENV['DATABASE_URL'])
|
||||
if args[:version]
|
||||
puts "Migrating to version #{args[:version]}"
|
||||
Sequel::Migrator.run(db, 'db/migrations', target: args[:version].to_i)
|
||||
else
|
||||
puts 'Migrating to latest'
|
||||
Sequel::Migrator.run(db, 'db/migrations')
|
||||
end
|
||||
puts 'Migration complete'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Testing tasks
|
||||
require 'rspec/core/rake_task'
|
||||
RSpec::Core::RakeTask.new(:spec)
|
||||
|
||||
task default: :spec
|
||||
```
|
||||
|
||||
**README.md:**
|
||||
```markdown
|
||||
# [Project Name]
|
||||
|
||||
[Brief description of the project]
|
||||
|
||||
## Setup
|
||||
|
||||
1. Install dependencies:
|
||||
```bash
|
||||
bundle install
|
||||
```
|
||||
|
||||
2. Set up environment variables:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Edit .env with your configuration
|
||||
```
|
||||
|
||||
3. Set up database (if applicable):
|
||||
```bash
|
||||
rake db:migrate
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
Run the application:
|
||||
```bash
|
||||
bundle exec rerun 'rackup -p 3000'
|
||||
```
|
||||
|
||||
Or with Puma:
|
||||
```bash
|
||||
bundle exec puma -C config/puma.rb
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Run tests:
|
||||
```bash
|
||||
bundle exec rspec
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
[Add deployment instructions]
|
||||
|
||||
## API Documentation
|
||||
|
||||
[Add API documentation if applicable]
|
||||
```
|
||||
|
||||
### Step 8: Initialize Git Repository
|
||||
|
||||
**Actions:**
|
||||
```bash
|
||||
cd project-name
|
||||
git init
|
||||
git add .
|
||||
git commit -m "Initial commit: Sinatra application scaffold"
|
||||
```
|
||||
|
||||
### Step 9: Install Dependencies
|
||||
|
||||
**Actions:**
|
||||
```bash
|
||||
bundle install
|
||||
```
|
||||
|
||||
**Verification:**
|
||||
- Confirm all gems installed successfully
|
||||
- Check for any dependency conflicts
|
||||
- Display next steps to user
|
||||
|
||||
## Expected Output
|
||||
|
||||
```
|
||||
Creating Sinatra application: my-app
|
||||
Type: modular
|
||||
Options: {"testing":"rspec","database":"sequel","frontend":"erb"}
|
||||
|
||||
✓ Created directory structure
|
||||
✓ Generated Gemfile
|
||||
✓ Created application files
|
||||
✓ Set up configuration files
|
||||
✓ Configured RSpec testing
|
||||
✓ Created supporting files
|
||||
✓ Initialized git repository
|
||||
✓ Installed dependencies
|
||||
|
||||
Application created successfully!
|
||||
|
||||
Next steps:
|
||||
cd my-app
|
||||
bundle exec rerun 'rackup -p 3000'
|
||||
|
||||
Visit: http://localhost:3000
|
||||
Tests: bundle exec rspec
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Invalid project name format
|
||||
- Directory already exists
|
||||
- Invalid JSON options
|
||||
- Bundle install failures
|
||||
- File creation permission errors
|
||||
|
||||
## Notes
|
||||
|
||||
- All generated code follows Ruby and Sinatra best practices
|
||||
- Testing framework is fully configured and ready to use
|
||||
- Development tools (rerun, pry) included for better DX
|
||||
- Production-ready configuration provided
|
||||
- Database migrations directory created if database selected
|
||||
- CORS configured for API applications
|
||||
860
commands/sinatra-test.md
Normal file
860
commands/sinatra-test.md
Normal file
@@ -0,0 +1,860 @@
|
||||
---
|
||||
description: Generate comprehensive tests for Sinatra routes, middleware, and helpers using RSpec or Minitest
|
||||
---
|
||||
|
||||
# Sinatra Test Command
|
||||
|
||||
Generates comprehensive test suites for Sinatra applications including route tests, middleware tests, helper tests, and integration tests using RSpec or Minitest.
|
||||
|
||||
## Arguments
|
||||
|
||||
- **$1: test-type** (optional) - Type of tests to generate: `routes`, `middleware`, `helpers`, or `all` (default: `all`)
|
||||
- **$2: framework** (optional) - Testing framework: `rspec` or `minitest` (default: `rspec`)
|
||||
|
||||
## Usage Examples
|
||||
|
||||
```bash
|
||||
# Generate all tests using RSpec
|
||||
/sinatra-test
|
||||
|
||||
# Generate only route tests with RSpec
|
||||
/sinatra-test routes
|
||||
|
||||
# Generate all tests using Minitest
|
||||
/sinatra-test all minitest
|
||||
|
||||
# Generate middleware tests with Minitest
|
||||
/sinatra-test middleware minitest
|
||||
|
||||
# Generate helper tests with RSpec
|
||||
/sinatra-test helpers rspec
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Analyze Application Structure
|
||||
|
||||
**Discovery Phase:**
|
||||
|
||||
1. Identify application type (classic vs modular)
|
||||
2. Locate controller files
|
||||
3. Extract route definitions
|
||||
4. Find middleware stack
|
||||
5. Identify helper methods
|
||||
6. Check existing test structure
|
||||
7. Detect testing framework if already configured
|
||||
|
||||
**Files to Analyze:**
|
||||
```ruby
|
||||
# Controllers
|
||||
app/controllers/**/*.rb
|
||||
app.rb (classic style)
|
||||
|
||||
# Middleware
|
||||
config.ru
|
||||
config/**/*.rb
|
||||
|
||||
# Helpers
|
||||
app/helpers/**/*.rb
|
||||
helpers/ directory
|
||||
|
||||
# Existing tests
|
||||
spec/**/*_spec.rb
|
||||
test/**/*_test.rb
|
||||
```
|
||||
|
||||
**Route Extraction:**
|
||||
```ruby
|
||||
# Parse routes from controller files
|
||||
# Identify: HTTP method, path, parameters, conditions
|
||||
|
||||
# Example routes to extract:
|
||||
get '/users' do
|
||||
# Handler
|
||||
end
|
||||
|
||||
get '/users/:id', :id => /\d+/ do
|
||||
# Handler with constraint
|
||||
end
|
||||
|
||||
post '/users', :provides => [:json] do
|
||||
# Handler with content negotiation
|
||||
end
|
||||
```
|
||||
|
||||
### Step 2: Generate Test Structure (RSpec)
|
||||
|
||||
**Create spec_helper.rb if missing:**
|
||||
|
||||
```ruby
|
||||
# spec/spec_helper.rb
|
||||
ENV['RACK_ENV'] = 'test'
|
||||
|
||||
require 'simplecov'
|
||||
SimpleCov.start do
|
||||
add_filter '/spec/'
|
||||
add_filter '/config/'
|
||||
end
|
||||
|
||||
require_relative '../config/environment'
|
||||
require 'rack/test'
|
||||
require 'rspec'
|
||||
require 'json'
|
||||
|
||||
# Database setup (if applicable)
|
||||
if defined?(Sequel)
|
||||
require 'database_cleaner/sequel'
|
||||
|
||||
RSpec.configure do |config|
|
||||
config.before(:suite) do
|
||||
DatabaseCleaner.strategy = :transaction
|
||||
DatabaseCleaner.clean_with(:truncation)
|
||||
end
|
||||
|
||||
config.around(:each) do |example|
|
||||
DatabaseCleaner.cleaning do
|
||||
example.run
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.configure do |config|
|
||||
config.include Rack::Test::Methods
|
||||
|
||||
config.expect_with :rspec do |expectations|
|
||||
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
||||
end
|
||||
|
||||
config.mock_with :rspec do |mocks|
|
||||
mocks.verify_partial_doubles = true
|
||||
end
|
||||
|
||||
config.shared_context_metadata_behavior = :apply_to_host_groups
|
||||
config.filter_run_when_matching :focus
|
||||
config.example_status_persistence_file_path = 'spec/examples.txt'
|
||||
config.disable_monkey_patching!
|
||||
config.warnings = true
|
||||
config.order = :random
|
||||
Kernel.srand config.seed
|
||||
end
|
||||
|
||||
# Helper methods for all specs
|
||||
module SpecHelpers
|
||||
def json_response
|
||||
JSON.parse(last_response.body)
|
||||
end
|
||||
|
||||
def auth_header(token)
|
||||
{ 'HTTP_AUTHORIZATION' => "Bearer #{token}" }
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.configure do |config|
|
||||
config.include SpecHelpers
|
||||
end
|
||||
```
|
||||
|
||||
**Create support files:**
|
||||
|
||||
```ruby
|
||||
# spec/support/factory_helper.rb (if using factories)
|
||||
require 'factory_bot'
|
||||
|
||||
RSpec.configure do |config|
|
||||
config.include FactoryBot::Syntax::Methods
|
||||
end
|
||||
|
||||
# spec/support/shared_examples.rb
|
||||
RSpec.shared_examples 'authenticated endpoint' do
|
||||
it 'returns 401 without authentication' do
|
||||
send(http_method, path)
|
||||
expect(last_response.status).to eq(401)
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'json endpoint' do
|
||||
it 'returns JSON content type' do
|
||||
send(http_method, path, valid_params)
|
||||
expect(last_response.content_type).to include('application/json')
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Step 3: Generate Route Tests
|
||||
|
||||
**For each route, generate comprehensive tests:**
|
||||
|
||||
```ruby
|
||||
# spec/controllers/users_controller_spec.rb
|
||||
require_relative '../spec_helper'
|
||||
|
||||
RSpec.describe UsersController do
|
||||
def app
|
||||
UsersController
|
||||
end
|
||||
|
||||
describe 'GET /users' do
|
||||
context 'with no users' do
|
||||
it 'returns empty array' do
|
||||
get '/users'
|
||||
expect(last_response).to be_ok
|
||||
expect(json_response).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
context 'with existing users' do
|
||||
let!(:users) { create_list(:user, 3) }
|
||||
|
||||
it 'returns all users' do
|
||||
get '/users'
|
||||
expect(last_response).to be_ok
|
||||
expect(json_response.length).to eq(3)
|
||||
end
|
||||
|
||||
it 'includes user attributes' do
|
||||
get '/users'
|
||||
user_data = json_response.first
|
||||
expect(user_data).to have_key('id')
|
||||
expect(user_data).to have_key('name')
|
||||
expect(user_data).to have_key('email')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with pagination' do
|
||||
let!(:users) { create_list(:user, 25) }
|
||||
|
||||
it 'respects page parameter' do
|
||||
get '/users?page=2&per_page=10'
|
||||
expect(json_response.length).to eq(10)
|
||||
end
|
||||
|
||||
it 'includes pagination metadata' do
|
||||
get '/users?page=1&per_page=10'
|
||||
expect(json_response['meta']).to include(
|
||||
'total' => 25,
|
||||
'page' => 1,
|
||||
'per_page' => 10
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with filtering' do
|
||||
let!(:active_user) { create(:user, active: true) }
|
||||
let!(:inactive_user) { create(:user, active: false) }
|
||||
|
||||
it 'filters by active status' do
|
||||
get '/users?active=true'
|
||||
expect(json_response.length).to eq(1)
|
||||
expect(json_response.first['id']).to eq(active_user.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /users/:id' do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
context 'when user exists' do
|
||||
it 'returns user details' do
|
||||
get "/users/#{user.id}"
|
||||
expect(last_response).to be_ok
|
||||
expect(json_response['id']).to eq(user.id)
|
||||
end
|
||||
|
||||
it 'includes all user attributes' do
|
||||
get "/users/#{user.id}"
|
||||
expect(json_response).to include(
|
||||
'id' => user.id,
|
||||
'name' => user.name,
|
||||
'email' => user.email
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user does not exist' do
|
||||
it 'returns 404' do
|
||||
get '/users/99999'
|
||||
expect(last_response.status).to eq(404)
|
||||
end
|
||||
|
||||
it 'returns error message' do
|
||||
get '/users/99999'
|
||||
expect(json_response).to include('error')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid id format' do
|
||||
it 'returns 404' do
|
||||
get '/users/invalid'
|
||||
expect(last_response.status).to eq(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /users' do
|
||||
let(:valid_attributes) do
|
||||
{
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
password: 'SecurePass123'
|
||||
}
|
||||
end
|
||||
|
||||
context 'with valid attributes' do
|
||||
it 'creates a new user' do
|
||||
expect {
|
||||
post '/users', valid_attributes.to_json,
|
||||
'CONTENT_TYPE' => 'application/json'
|
||||
}.to change(User, :count).by(1)
|
||||
end
|
||||
|
||||
it 'returns 201 status' do
|
||||
post '/users', valid_attributes.to_json,
|
||||
'CONTENT_TYPE' => 'application/json'
|
||||
expect(last_response.status).to eq(201)
|
||||
end
|
||||
|
||||
it 'returns created user' do
|
||||
post '/users', valid_attributes.to_json,
|
||||
'CONTENT_TYPE' => 'application/json'
|
||||
expect(json_response).to include(
|
||||
'name' => 'John Doe',
|
||||
'email' => 'john@example.com'
|
||||
)
|
||||
end
|
||||
|
||||
it 'does not return password' do
|
||||
post '/users', valid_attributes.to_json,
|
||||
'CONTENT_TYPE' => 'application/json'
|
||||
expect(json_response).not_to have_key('password')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid attributes' do
|
||||
it 'returns 422 status' do
|
||||
post '/users', { name: '' }.to_json,
|
||||
'CONTENT_TYPE' => 'application/json'
|
||||
expect(last_response.status).to eq(422)
|
||||
end
|
||||
|
||||
it 'returns validation errors' do
|
||||
post '/users', { name: '' }.to_json,
|
||||
'CONTENT_TYPE' => 'application/json'
|
||||
expect(json_response).to have_key('errors')
|
||||
end
|
||||
|
||||
it 'does not create user' do
|
||||
expect {
|
||||
post '/users', { name: '' }.to_json,
|
||||
'CONTENT_TYPE' => 'application/json'
|
||||
}.not_to change(User, :count)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with duplicate email' do
|
||||
let!(:existing_user) { create(:user, email: 'john@example.com') }
|
||||
|
||||
it 'returns 422 status' do
|
||||
post '/users', valid_attributes.to_json,
|
||||
'CONTENT_TYPE' => 'application/json'
|
||||
expect(last_response.status).to eq(422)
|
||||
end
|
||||
|
||||
it 'returns uniqueness error' do
|
||||
post '/users', valid_attributes.to_json,
|
||||
'CONTENT_TYPE' => 'application/json'
|
||||
expect(json_response['errors']).to include('email')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PUT /users/:id' do
|
||||
let(:user) { create(:user) }
|
||||
let(:update_attributes) { { name: 'Updated Name' } }
|
||||
|
||||
context 'when user exists' do
|
||||
it 'updates user attributes' do
|
||||
put "/users/#{user.id}", update_attributes.to_json,
|
||||
'CONTENT_TYPE' => 'application/json'
|
||||
user.reload
|
||||
expect(user.name).to eq('Updated Name')
|
||||
end
|
||||
|
||||
it 'returns 200 status' do
|
||||
put "/users/#{user.id}", update_attributes.to_json,
|
||||
'CONTENT_TYPE' => 'application/json'
|
||||
expect(last_response).to be_ok
|
||||
end
|
||||
|
||||
it 'returns updated user' do
|
||||
put "/users/#{user.id}", update_attributes.to_json,
|
||||
'CONTENT_TYPE' => 'application/json'
|
||||
expect(json_response['name']).to eq('Updated Name')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid attributes' do
|
||||
it 'returns 422 status' do
|
||||
put "/users/#{user.id}", { email: 'invalid' }.to_json,
|
||||
'CONTENT_TYPE' => 'application/json'
|
||||
expect(last_response.status).to eq(422)
|
||||
end
|
||||
|
||||
it 'does not update user' do
|
||||
original_email = user.email
|
||||
put "/users/#{user.id}", { email: 'invalid' }.to_json,
|
||||
'CONTENT_TYPE' => 'application/json'
|
||||
user.reload
|
||||
expect(user.email).to eq(original_email)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user does not exist' do
|
||||
it 'returns 404' do
|
||||
put '/users/99999', update_attributes.to_json,
|
||||
'CONTENT_TYPE' => 'application/json'
|
||||
expect(last_response.status).to eq(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE /users/:id' do
|
||||
let!(:user) { create(:user) }
|
||||
|
||||
context 'when user exists' do
|
||||
it 'deletes the user' do
|
||||
expect {
|
||||
delete "/users/#{user.id}"
|
||||
}.to change(User, :count).by(-1)
|
||||
end
|
||||
|
||||
it 'returns 204 status' do
|
||||
delete "/users/#{user.id}"
|
||||
expect(last_response.status).to eq(204)
|
||||
end
|
||||
|
||||
it 'returns empty body' do
|
||||
delete "/users/#{user.id}"
|
||||
expect(last_response.body).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user does not exist' do
|
||||
it 'returns 404' do
|
||||
delete '/users/99999'
|
||||
expect(last_response.status).to eq(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Authentication tests
|
||||
describe 'authentication' do
|
||||
let(:protected_path) { '/users' }
|
||||
let(:http_method) { :get }
|
||||
let(:path) { protected_path }
|
||||
|
||||
it_behaves_like 'authenticated endpoint'
|
||||
end
|
||||
|
||||
# Content negotiation tests
|
||||
describe 'content negotiation' do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
context 'with Accept: application/json' do
|
||||
it 'returns JSON' do
|
||||
get "/users/#{user.id}", {}, { 'HTTP_ACCEPT' => 'application/json' }
|
||||
expect(last_response.content_type).to include('application/json')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with Accept: application/xml' do
|
||||
it 'returns XML' do
|
||||
get "/users/#{user.id}", {}, { 'HTTP_ACCEPT' => 'application/xml' }
|
||||
expect(last_response.content_type).to include('application/xml')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Step 4: Generate Middleware Tests
|
||||
|
||||
```ruby
|
||||
# spec/middleware/custom_middleware_spec.rb
|
||||
require_relative '../spec_helper'
|
||||
|
||||
RSpec.describe CustomMiddleware do
|
||||
let(:app) { ->(env) { [200, {}, ['OK']] } }
|
||||
let(:middleware) { CustomMiddleware.new(app) }
|
||||
let(:request) { Rack::MockRequest.new(middleware) }
|
||||
|
||||
describe 'request processing' do
|
||||
it 'passes request to next middleware' do
|
||||
response = request.get('/')
|
||||
expect(response.status).to eq(200)
|
||||
end
|
||||
|
||||
it 'adds custom header to response' do
|
||||
response = request.get('/')
|
||||
expect(response.headers['X-Custom-Header']).to eq('value')
|
||||
end
|
||||
|
||||
it 'modifies request environment' do
|
||||
env = {}
|
||||
middleware.call(env)
|
||||
expect(env['custom.key']).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
describe 'error handling' do
|
||||
let(:app) { ->(env) { raise StandardError, 'Error' } }
|
||||
|
||||
it 'catches errors from downstream' do
|
||||
response = request.get('/')
|
||||
expect(response.status).to eq(500)
|
||||
end
|
||||
|
||||
it 'logs error' do
|
||||
expect { request.get('/') }.to change { error_log.size }.by(1)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'configuration' do
|
||||
let(:middleware) { CustomMiddleware.new(app, option: 'value') }
|
||||
|
||||
it 'accepts configuration options' do
|
||||
expect(middleware.options[:option]).to eq('value')
|
||||
end
|
||||
|
||||
it 'applies configuration to behavior' do
|
||||
response = request.get('/')
|
||||
expect(response.headers['X-Option']).to eq('value')
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Step 5: Generate Helper Tests
|
||||
|
||||
```ruby
|
||||
# spec/helpers/application_helpers_spec.rb
|
||||
require_relative '../spec_helper'
|
||||
|
||||
RSpec.describe ApplicationHelpers do
|
||||
let(:dummy_class) do
|
||||
Class.new do
|
||||
include ApplicationHelpers
|
||||
|
||||
# Mock request/session for helper context
|
||||
def request
|
||||
@request ||= Struct.new(:path_info).new('/test')
|
||||
end
|
||||
|
||||
def session
|
||||
@session ||= {}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
let(:helpers) { dummy_class.new }
|
||||
|
||||
describe '#current_user' do
|
||||
context 'when user is logged in' do
|
||||
before do
|
||||
helpers.session[:user_id] = 1
|
||||
allow(User).to receive(:find).with(1).and_return(
|
||||
double('User', id: 1, name: 'John')
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns current user' do
|
||||
expect(helpers.current_user).to be_present
|
||||
expect(helpers.current_user.id).to eq(1)
|
||||
end
|
||||
|
||||
it 'memoizes user' do
|
||||
expect(User).to receive(:find).once
|
||||
helpers.current_user
|
||||
helpers.current_user
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is not logged in' do
|
||||
it 'returns nil' do
|
||||
expect(helpers.current_user).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#logged_in?' do
|
||||
it 'returns true when current_user exists' do
|
||||
allow(helpers).to receive(:current_user).and_return(double('User'))
|
||||
expect(helpers.logged_in?).to be true
|
||||
end
|
||||
|
||||
it 'returns false when current_user is nil' do
|
||||
allow(helpers).to receive(:current_user).and_return(nil)
|
||||
expect(helpers.logged_in?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe '#format_date' do
|
||||
let(:date) { Time.new(2024, 1, 15, 10, 30, 0) }
|
||||
|
||||
it 'formats date with default format' do
|
||||
expect(helpers.format_date(date)).to eq('2024-01-15')
|
||||
end
|
||||
|
||||
it 'accepts custom format' do
|
||||
expect(helpers.format_date(date, '%m/%d/%Y')).to eq('01/15/2024')
|
||||
end
|
||||
|
||||
it 'handles nil date' do
|
||||
expect(helpers.format_date(nil)).to eq('')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#truncate' do
|
||||
let(:long_text) { 'This is a very long text that should be truncated' }
|
||||
|
||||
it 'truncates text to specified length' do
|
||||
expect(helpers.truncate(long_text, 20)).to eq('This is a very long...')
|
||||
end
|
||||
|
||||
it 'does not truncate short text' do
|
||||
short_text = 'Short'
|
||||
expect(helpers.truncate(short_text, 20)).to eq('Short')
|
||||
end
|
||||
|
||||
it 'accepts custom omission' do
|
||||
expect(helpers.truncate(long_text, 20, omission: '…')).to include('…')
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Step 6: Generate Minitest Tests (Alternative)
|
||||
|
||||
**If framework is Minitest:**
|
||||
|
||||
```ruby
|
||||
# test/test_helper.rb
|
||||
ENV['RACK_ENV'] = 'test'
|
||||
|
||||
require 'simplecov'
|
||||
SimpleCov.start
|
||||
|
||||
require_relative '../config/environment'
|
||||
require 'minitest/autorun'
|
||||
require 'minitest/spec'
|
||||
require 'rack/test'
|
||||
|
||||
class Minitest::Spec
|
||||
include Rack::Test::Methods
|
||||
|
||||
def json_response
|
||||
JSON.parse(last_response.body)
|
||||
end
|
||||
end
|
||||
|
||||
# test/controllers/users_controller_test.rb
|
||||
require_relative '../test_helper'
|
||||
|
||||
describe UsersController do
|
||||
def app
|
||||
UsersController
|
||||
end
|
||||
|
||||
describe 'GET /users' do
|
||||
it 'returns success' do
|
||||
get '/users'
|
||||
assert last_response.ok?
|
||||
end
|
||||
|
||||
it 'returns JSON' do
|
||||
get '/users'
|
||||
assert_includes last_response.content_type, 'application/json'
|
||||
end
|
||||
|
||||
describe 'with existing users' do
|
||||
before do
|
||||
@users = 3.times.map { User.create(name: 'Test') }
|
||||
end
|
||||
|
||||
it 'returns all users' do
|
||||
get '/users'
|
||||
assert_equal 3, json_response.length
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /users' do
|
||||
let(:valid_params) { { name: 'John', email: 'john@example.com' } }
|
||||
|
||||
it 'creates user' do
|
||||
assert_difference 'User.count', 1 do
|
||||
post '/users', valid_params.to_json,
|
||||
'CONTENT_TYPE' => 'application/json'
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns 201' do
|
||||
post '/users', valid_params.to_json,
|
||||
'CONTENT_TYPE' => 'application/json'
|
||||
assert_equal 201, last_response.status
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Step 7: Generate Integration Tests
|
||||
|
||||
```ruby
|
||||
# spec/integration/user_registration_spec.rb
|
||||
require_relative '../spec_helper'
|
||||
|
||||
RSpec.describe 'User Registration Flow' do
|
||||
def app
|
||||
Sinatra::Application
|
||||
end
|
||||
|
||||
describe 'complete registration process' do
|
||||
let(:user_params) do
|
||||
{
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
password: 'SecurePass123'
|
||||
}
|
||||
end
|
||||
|
||||
it 'allows new user to register and log in' do
|
||||
# Step 1: Register
|
||||
post '/register', user_params.to_json,
|
||||
'CONTENT_TYPE' => 'application/json'
|
||||
expect(last_response.status).to eq(201)
|
||||
|
||||
user_id = json_response['id']
|
||||
|
||||
# Step 2: Verify email confirmation sent
|
||||
expect(EmailService.last_email[:to]).to eq('john@example.com')
|
||||
|
||||
# Step 3: Confirm email
|
||||
token = EmailService.last_email[:token]
|
||||
get "/confirm/#{token}"
|
||||
expect(last_response.status).to eq(200)
|
||||
|
||||
# Step 4: Log in
|
||||
post '/login', { email: 'john@example.com', password: 'SecurePass123' }.to_json,
|
||||
'CONTENT_TYPE' => 'application/json'
|
||||
expect(last_response.status).to eq(200)
|
||||
expect(json_response).to have_key('token')
|
||||
|
||||
# Step 5: Access protected resource
|
||||
token = json_response['token']
|
||||
get '/profile', {}, auth_header(token)
|
||||
expect(last_response).to be_ok
|
||||
expect(json_response['id']).to eq(user_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Step 8: Create Test Documentation
|
||||
|
||||
**Generate test README:**
|
||||
|
||||
```markdown
|
||||
# Test Suite Documentation
|
||||
|
||||
## Running Tests
|
||||
|
||||
### All Tests
|
||||
```bash
|
||||
bundle exec rspec
|
||||
```
|
||||
|
||||
### Specific Test File
|
||||
```bash
|
||||
bundle exec rspec spec/controllers/users_controller_spec.rb
|
||||
```
|
||||
|
||||
### By Tag
|
||||
```bash
|
||||
bundle exec rspec --tag focus
|
||||
```
|
||||
|
||||
## Test Structure
|
||||
|
||||
- `spec/controllers/` - Route and controller tests
|
||||
- `spec/middleware/` - Middleware tests
|
||||
- `spec/helpers/` - Helper method tests
|
||||
- `spec/models/` - Model tests (if applicable)
|
||||
- `spec/integration/` - End-to-end integration tests
|
||||
- `spec/support/` - Shared examples and helpers
|
||||
|
||||
## Coverage
|
||||
|
||||
Run tests with coverage report:
|
||||
```bash
|
||||
COVERAGE=true bundle exec rspec
|
||||
```
|
||||
|
||||
View coverage report:
|
||||
```bash
|
||||
open coverage/index.html
|
||||
```
|
||||
|
||||
## Testing Patterns
|
||||
|
||||
### Route Testing
|
||||
- Test successful responses
|
||||
- Test error cases (404, 422, 500)
|
||||
- Test authentication/authorization
|
||||
- Test parameter validation
|
||||
- Test content negotiation
|
||||
|
||||
### Helper Testing
|
||||
- Test with various inputs
|
||||
- Test edge cases
|
||||
- Test nil handling
|
||||
- Mock dependencies
|
||||
|
||||
### Integration Testing
|
||||
- Test complete user flows
|
||||
- Test interactions between components
|
||||
- Test external service integration
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
**Generated files report:**
|
||||
```
|
||||
Test Generation Complete!
|
||||
|
||||
Framework: RSpec
|
||||
Test Type: all
|
||||
|
||||
Generated Files:
|
||||
✓ spec/spec_helper.rb
|
||||
✓ spec/support/factory_helper.rb
|
||||
✓ spec/support/shared_examples.rb
|
||||
✓ spec/controllers/users_controller_spec.rb (45 examples)
|
||||
✓ spec/controllers/posts_controller_spec.rb (38 examples)
|
||||
✓ spec/middleware/custom_middleware_spec.rb (12 examples)
|
||||
✓ spec/helpers/application_helpers_spec.rb (15 examples)
|
||||
✓ spec/integration/user_registration_spec.rb (5 examples)
|
||||
✓ TEST_README.md
|
||||
|
||||
Total Examples: 115
|
||||
Coverage Target: 90%
|
||||
|
||||
Run tests: bundle exec rspec
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Handle applications without routes gracefully
|
||||
- Skip already existing test files (or offer to overwrite)
|
||||
- Detect testing framework from Gemfile
|
||||
- Warn if test dependencies missing
|
||||
- Handle parse errors in application files
|
||||
Reference in New Issue
Block a user