Initial commit
This commit is contained in:
656
skills/sinatra-patterns/SKILL.md
Normal file
656
skills/sinatra-patterns/SKILL.md
Normal file
@@ -0,0 +1,656 @@
|
||||
---
|
||||
name: sinatra-patterns
|
||||
description: Common Sinatra patterns, routing strategies, error handling, and application organization. Use when building Sinatra applications or designing routes.
|
||||
---
|
||||
|
||||
# Sinatra Patterns Skill
|
||||
|
||||
## Tier 1: Quick Reference
|
||||
|
||||
### Common Routing Patterns
|
||||
|
||||
**Basic Routes:**
|
||||
```ruby
|
||||
get '/' do
|
||||
'Hello World'
|
||||
end
|
||||
|
||||
post '/users' do
|
||||
# Create user
|
||||
end
|
||||
|
||||
put '/users/:id' do
|
||||
# Update user
|
||||
end
|
||||
|
||||
delete '/users/:id' do
|
||||
# Delete user
|
||||
end
|
||||
```
|
||||
|
||||
**Route Parameters:**
|
||||
```ruby
|
||||
# Named parameters
|
||||
get '/users/:id' do
|
||||
User.find(params[:id])
|
||||
end
|
||||
|
||||
# Parameter constraints
|
||||
get '/users/:id', :id => /\d+/ do
|
||||
# Only matches numeric IDs
|
||||
end
|
||||
|
||||
# Wildcard
|
||||
get '/files/*.*' do
|
||||
# params['splat'] contains matched segments
|
||||
end
|
||||
```
|
||||
|
||||
**Query Parameters:**
|
||||
```ruby
|
||||
get '/search' do
|
||||
query = params[:q]
|
||||
page = params[:page] || 1
|
||||
results = search(query, page: page)
|
||||
end
|
||||
```
|
||||
|
||||
### Basic Middleware
|
||||
|
||||
```ruby
|
||||
# Session middleware
|
||||
use Rack::Session::Cookie, secret: ENV['SESSION_SECRET']
|
||||
|
||||
# Security middleware
|
||||
use Rack::Protection
|
||||
|
||||
# Logging
|
||||
use Rack::CommonLogger
|
||||
|
||||
# Compression
|
||||
use Rack::Deflater
|
||||
```
|
||||
|
||||
### Simple Error Handling
|
||||
|
||||
```ruby
|
||||
not_found do
|
||||
'Page not found'
|
||||
end
|
||||
|
||||
error do
|
||||
'Internal server error'
|
||||
end
|
||||
|
||||
error 401 do
|
||||
'Unauthorized'
|
||||
end
|
||||
```
|
||||
|
||||
### Helpers
|
||||
|
||||
```ruby
|
||||
helpers do
|
||||
def logged_in?
|
||||
!session[:user_id].nil?
|
||||
end
|
||||
|
||||
def current_user
|
||||
@current_user ||= User.find_by(id: session[:user_id])
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tier 2: Detailed Instructions
|
||||
|
||||
### Advanced Routing
|
||||
|
||||
**Modular Applications:**
|
||||
```ruby
|
||||
# app/controllers/base_controller.rb
|
||||
class BaseController < Sinatra::Base
|
||||
configure do
|
||||
set :views, Proc.new { File.join(root, '../views') }
|
||||
set :public_folder, Proc.new { File.join(root, '../public') }
|
||||
end
|
||||
|
||||
helpers do
|
||||
def json_response(data, status = 200)
|
||||
content_type :json
|
||||
halt status, data.to_json
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# app/controllers/users_controller.rb
|
||||
class UsersController < BaseController
|
||||
get '/' do
|
||||
users = User.all
|
||||
json_response(users.map(&:to_hash))
|
||||
end
|
||||
|
||||
get '/:id' do
|
||||
user = User.find(params[:id]) || halt(404)
|
||||
json_response(user.to_hash)
|
||||
end
|
||||
|
||||
post '/' do
|
||||
user = User.create(params[:user])
|
||||
if user.persisted?
|
||||
json_response(user.to_hash, 201)
|
||||
else
|
||||
json_response({ errors: user.errors }, 422)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# config.ru
|
||||
map '/users' do
|
||||
run UsersController
|
||||
end
|
||||
```
|
||||
|
||||
**Namespaces:**
|
||||
```ruby
|
||||
require 'sinatra/namespace'
|
||||
|
||||
class App < Sinatra::Base
|
||||
register Sinatra::Namespace
|
||||
|
||||
namespace '/api' do
|
||||
namespace '/v1' do
|
||||
get '/users' do
|
||||
# GET /api/v1/users
|
||||
end
|
||||
|
||||
namespace '/admin' do
|
||||
before do
|
||||
authenticate_admin!
|
||||
end
|
||||
|
||||
get '/stats' do
|
||||
# GET /api/v1/admin/stats
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Route Conditions:**
|
||||
```ruby
|
||||
# User agent condition
|
||||
get '/', :agent => /iPhone/ do
|
||||
# Mobile version
|
||||
end
|
||||
|
||||
# Custom conditions
|
||||
set(:auth) do |role|
|
||||
condition do
|
||||
unless current_user && current_user.has_role?(role)
|
||||
halt 403
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
get '/admin', :auth => :admin do
|
||||
# Only accessible to admins
|
||||
end
|
||||
|
||||
# Host-based routing
|
||||
get '/', :host => 'admin.example.com' do
|
||||
# Admin subdomain
|
||||
end
|
||||
```
|
||||
|
||||
**Content Negotiation:**
|
||||
```ruby
|
||||
get '/users/:id', :provides => [:json, :xml, :html] do
|
||||
user = User.find(params[:id])
|
||||
|
||||
case request.accept.first.to_s
|
||||
when 'application/json'
|
||||
json user.to_json
|
||||
when 'application/xml'
|
||||
xml user.to_xml
|
||||
else
|
||||
erb :user, locals: { user: user }
|
||||
end
|
||||
end
|
||||
|
||||
# Or using provides helper
|
||||
get '/users/:id' do
|
||||
user = User.find(params[:id])
|
||||
|
||||
respond_to do |format|
|
||||
format.json { json user.to_json }
|
||||
format.xml { xml user.to_xml }
|
||||
format.html { erb :user, locals: { user: user } }
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Middleware Composition
|
||||
|
||||
**Custom Middleware:**
|
||||
```ruby
|
||||
class RequestLogger
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
start_time = Time.now
|
||||
status, headers, body = @app.call(env)
|
||||
duration = Time.now - start_time
|
||||
|
||||
puts "#{env['REQUEST_METHOD']} #{env['PATH_INFO']} - #{status} (#{duration}s)"
|
||||
|
||||
[status, headers, body]
|
||||
end
|
||||
end
|
||||
|
||||
use RequestLogger
|
||||
```
|
||||
|
||||
**Middleware Ordering:**
|
||||
```ruby
|
||||
# config.ru
|
||||
use Rack::Deflater # Compression first
|
||||
use Rack::Static # Static files
|
||||
use Rack::CommonLogger # Logging
|
||||
use Rack::Session::Cookie # Sessions
|
||||
use Rack::Protection # Security
|
||||
use CustomAuthentication # Auth
|
||||
run Application
|
||||
```
|
||||
|
||||
### Template Integration
|
||||
|
||||
**ERB Templates:**
|
||||
```ruby
|
||||
# views/layout.erb
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title><%= @title || 'My App' %></title>
|
||||
</head>
|
||||
<body>
|
||||
<%= yield %>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
# views/users/index.erb
|
||||
<h1>Users</h1>
|
||||
<ul>
|
||||
<% @users.each do |user| %>
|
||||
<li><%= user.name %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
|
||||
# Controller
|
||||
get '/users' do
|
||||
@users = User.all
|
||||
@title = 'User List'
|
||||
erb :'users/index'
|
||||
end
|
||||
```
|
||||
|
||||
**Inline Templates:**
|
||||
```ruby
|
||||
get '/' do
|
||||
erb :index
|
||||
end
|
||||
|
||||
__END__
|
||||
|
||||
@@layout
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body><%= yield %></body>
|
||||
</html>
|
||||
|
||||
@@index
|
||||
<h1>Welcome</h1>
|
||||
```
|
||||
|
||||
**Template Engines:**
|
||||
```ruby
|
||||
# Haml
|
||||
get '/' do
|
||||
haml :index
|
||||
end
|
||||
|
||||
# Slim
|
||||
get '/' do
|
||||
slim :index
|
||||
end
|
||||
|
||||
# Liquid (safe for user content)
|
||||
get '/' do
|
||||
liquid :index, locals: { user: current_user }
|
||||
end
|
||||
```
|
||||
|
||||
### Error Handling Patterns
|
||||
|
||||
**Comprehensive Error Handling:**
|
||||
```ruby
|
||||
class Application < Sinatra::Base
|
||||
# Development configuration
|
||||
configure :development do
|
||||
set :show_exceptions, :after_handler
|
||||
set :dump_errors, true
|
||||
end
|
||||
|
||||
# Production configuration
|
||||
configure :production do
|
||||
set :show_exceptions, false
|
||||
set :dump_errors, false
|
||||
end
|
||||
|
||||
# Specific exception handlers
|
||||
error ActiveRecord::RecordNotFound do
|
||||
status 404
|
||||
json({ error: 'Resource not found' })
|
||||
end
|
||||
|
||||
error ActiveRecord::RecordInvalid do
|
||||
status 422
|
||||
json({ error: 'Validation failed', details: env['sinatra.error'].message })
|
||||
end
|
||||
|
||||
error Sequel::NoMatchingRow do
|
||||
status 404
|
||||
json({ error: 'Not found' })
|
||||
end
|
||||
|
||||
# HTTP status handlers
|
||||
not_found do
|
||||
json({ error: 'Endpoint not found' })
|
||||
end
|
||||
|
||||
error 401 do
|
||||
json({ error: 'Unauthorized' })
|
||||
end
|
||||
|
||||
error 403 do
|
||||
json({ error: 'Forbidden' })
|
||||
end
|
||||
|
||||
error 422 do
|
||||
json({ error: 'Unprocessable entity' })
|
||||
end
|
||||
|
||||
# Catch-all error handler
|
||||
error do
|
||||
error = env['sinatra.error']
|
||||
logger.error("Error: #{error.message}")
|
||||
logger.error(error.backtrace.join("\n"))
|
||||
|
||||
status 500
|
||||
json({ error: 'Internal server error' })
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Before/After Filters
|
||||
|
||||
**Request Filters:**
|
||||
```ruby
|
||||
# Global before filter
|
||||
before do
|
||||
content_type :json
|
||||
end
|
||||
|
||||
# Path-specific filters
|
||||
before '/admin/*' do
|
||||
authenticate_admin!
|
||||
end
|
||||
|
||||
# Conditional filters
|
||||
before do
|
||||
pass unless request.path.start_with?('/api')
|
||||
authenticate_api_user!
|
||||
end
|
||||
|
||||
# After filters
|
||||
after do
|
||||
# Add CORS headers
|
||||
headers 'Access-Control-Allow-Origin' => '*'
|
||||
end
|
||||
|
||||
# Modify response
|
||||
after do
|
||||
response.body = response.body.map(&:upcase) if params[:uppercase]
|
||||
end
|
||||
```
|
||||
|
||||
### Session Management
|
||||
|
||||
**Cookie Sessions:**
|
||||
```ruby
|
||||
use Rack::Session::Cookie,
|
||||
key: 'app.session',
|
||||
secret: ENV['SESSION_SECRET'],
|
||||
expire_after: 86400, # 1 day
|
||||
secure: production?,
|
||||
httponly: true,
|
||||
same_site: :strict
|
||||
|
||||
helpers do
|
||||
def login(user)
|
||||
session[:user_id] = user.id
|
||||
session[:logged_in_at] = Time.now.to_i
|
||||
end
|
||||
|
||||
def logout
|
||||
session.clear
|
||||
end
|
||||
|
||||
def current_user
|
||||
return nil unless session[:user_id]
|
||||
@current_user ||= User.find_by(id: session[:user_id])
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tier 3: Resources & Examples
|
||||
|
||||
### Full Application Example
|
||||
|
||||
See `assets/modular-app-template/` for complete modular application structure.
|
||||
|
||||
### Performance Patterns
|
||||
|
||||
**Caching:**
|
||||
```ruby
|
||||
# HTTP caching
|
||||
get '/public/data' do
|
||||
cache_control :public, max_age: 3600
|
||||
etag calculate_etag
|
||||
last_modified last_update_time
|
||||
|
||||
json PublicData.all.map(&:to_hash)
|
||||
end
|
||||
|
||||
# Fragment caching with Redis
|
||||
require 'redis'
|
||||
|
||||
helpers do
|
||||
def cache_fetch(key, expires_in: 300, &block)
|
||||
cached = REDIS.get(key)
|
||||
return JSON.parse(cached) if cached
|
||||
|
||||
data = block.call
|
||||
REDIS.setex(key, expires_in, data.to_json)
|
||||
data
|
||||
end
|
||||
end
|
||||
|
||||
get '/expensive-data' do
|
||||
data = cache_fetch('expensive-data', expires_in: 600) do
|
||||
perform_expensive_query
|
||||
end
|
||||
|
||||
json data
|
||||
end
|
||||
```
|
||||
|
||||
**Streaming Responses:**
|
||||
```ruby
|
||||
# Stream large responses
|
||||
get '/large-export' do
|
||||
stream do |out|
|
||||
User.find_each do |user|
|
||||
out << user.to_csv_row
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Server-Sent Events
|
||||
get '/events', provides: 'text/event-stream' do
|
||||
stream :keep_open do |out|
|
||||
EventSource.subscribe do |event|
|
||||
out << "data: #{event.to_json}\n\n"
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Production Configuration
|
||||
|
||||
**Complete config.ru:**
|
||||
```ruby
|
||||
# config.ru
|
||||
require_relative 'config/environment'
|
||||
|
||||
# Production middleware
|
||||
if ENV['RACK_ENV'] == 'production'
|
||||
use Rack::SSL
|
||||
use Rack::Deflater
|
||||
end
|
||||
|
||||
# Static files
|
||||
use Rack::Static,
|
||||
urls: ['/css', '/js', '/images'],
|
||||
root: 'public',
|
||||
header_rules: [
|
||||
[:all, {'Cache-Control' => 'public, max-age=31536000'}]
|
||||
]
|
||||
|
||||
# Logging
|
||||
use Rack::CommonLogger
|
||||
|
||||
# Sessions
|
||||
use Rack::Session::Cookie,
|
||||
secret: ENV['SESSION_SECRET'],
|
||||
same_site: :strict,
|
||||
httponly: true,
|
||||
secure: ENV['RACK_ENV'] == 'production'
|
||||
|
||||
# Security
|
||||
use Rack::Protection,
|
||||
except: [:session_hijacking],
|
||||
use: :all
|
||||
|
||||
# Rate limiting (production)
|
||||
if ENV['RACK_ENV'] == 'production'
|
||||
require 'rack/attack'
|
||||
use Rack::Attack
|
||||
end
|
||||
|
||||
# Mount applications
|
||||
map '/api/v1' do
|
||||
run ApiV1::Application
|
||||
end
|
||||
|
||||
map '/' do
|
||||
run WebApplication
|
||||
end
|
||||
```
|
||||
|
||||
**Rack::Attack Configuration:**
|
||||
```ruby
|
||||
# config/rack_attack.rb
|
||||
class Rack::Attack
|
||||
# Throttle login attempts
|
||||
throttle('login/ip', limit: 5, period: 60) do |req|
|
||||
req.ip if req.path == '/login' && req.post?
|
||||
end
|
||||
|
||||
# Throttle API requests
|
||||
throttle('api/ip', limit: 100, period: 60) do |req|
|
||||
req.ip if req.path.start_with?('/api')
|
||||
end
|
||||
|
||||
# Block suspicious requests
|
||||
blocklist('block bad user agents') do |req|
|
||||
req.user_agent =~ /bad_bot/i
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Testing Patterns
|
||||
|
||||
See `references/testing-examples.rb` for comprehensive test patterns.
|
||||
|
||||
### Project Structure
|
||||
|
||||
**Recommended modular structure:**
|
||||
```
|
||||
app/
|
||||
controllers/
|
||||
base_controller.rb
|
||||
api_controller.rb
|
||||
users_controller.rb
|
||||
posts_controller.rb
|
||||
models/
|
||||
user.rb
|
||||
post.rb
|
||||
services/
|
||||
user_service.rb
|
||||
authentication_service.rb
|
||||
helpers/
|
||||
application_helpers.rb
|
||||
view_helpers.rb
|
||||
config/
|
||||
environment.rb
|
||||
database.yml
|
||||
puma.rb
|
||||
db/
|
||||
migrations/
|
||||
lib/
|
||||
middleware/
|
||||
custom_auth.rb
|
||||
tasks/
|
||||
public/
|
||||
css/
|
||||
js/
|
||||
images/
|
||||
views/
|
||||
layout.erb
|
||||
users/
|
||||
index.erb
|
||||
show.erb
|
||||
spec/
|
||||
controllers/
|
||||
models/
|
||||
spec_helper.rb
|
||||
config.ru
|
||||
Gemfile
|
||||
Rakefile
|
||||
README.md
|
||||
```
|
||||
|
||||
### Additional Resources
|
||||
|
||||
- **Routing Examples:** `assets/routing-examples.rb`
|
||||
- **Middleware Patterns:** `assets/middleware-patterns.rb`
|
||||
- **Modular App Template:** `assets/modular-app-template/`
|
||||
- **Production Config:** `references/production-config.rb`
|
||||
- **Testing Guide:** `references/testing-examples.rb`
|
||||
Reference in New Issue
Block a user