tarsier 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +175 -0
- data/LICENSE.txt +21 -0
- data/README.md +984 -0
- data/exe/tarsier +7 -0
- data/lib/tarsier/application.rb +336 -0
- data/lib/tarsier/cli/commands/console.rb +87 -0
- data/lib/tarsier/cli/commands/generate.rb +85 -0
- data/lib/tarsier/cli/commands/help.rb +50 -0
- data/lib/tarsier/cli/commands/new.rb +59 -0
- data/lib/tarsier/cli/commands/routes.rb +139 -0
- data/lib/tarsier/cli/commands/server.rb +123 -0
- data/lib/tarsier/cli/commands/version.rb +14 -0
- data/lib/tarsier/cli/generators/app.rb +528 -0
- data/lib/tarsier/cli/generators/base.rb +93 -0
- data/lib/tarsier/cli/generators/controller.rb +91 -0
- data/lib/tarsier/cli/generators/middleware.rb +81 -0
- data/lib/tarsier/cli/generators/migration.rb +109 -0
- data/lib/tarsier/cli/generators/model.rb +109 -0
- data/lib/tarsier/cli/generators/resource.rb +27 -0
- data/lib/tarsier/cli/loader.rb +18 -0
- data/lib/tarsier/cli.rb +46 -0
- data/lib/tarsier/controller.rb +282 -0
- data/lib/tarsier/database.rb +588 -0
- data/lib/tarsier/errors.rb +77 -0
- data/lib/tarsier/middleware/base.rb +47 -0
- data/lib/tarsier/middleware/compression.rb +113 -0
- data/lib/tarsier/middleware/cors.rb +101 -0
- data/lib/tarsier/middleware/csrf.rb +88 -0
- data/lib/tarsier/middleware/logger.rb +74 -0
- data/lib/tarsier/middleware/rate_limit.rb +110 -0
- data/lib/tarsier/middleware/stack.rb +143 -0
- data/lib/tarsier/middleware/static.rb +124 -0
- data/lib/tarsier/model.rb +590 -0
- data/lib/tarsier/params.rb +269 -0
- data/lib/tarsier/query.rb +495 -0
- data/lib/tarsier/request.rb +274 -0
- data/lib/tarsier/response.rb +282 -0
- data/lib/tarsier/router/compiler.rb +173 -0
- data/lib/tarsier/router/node.rb +97 -0
- data/lib/tarsier/router/route.rb +119 -0
- data/lib/tarsier/router.rb +272 -0
- data/lib/tarsier/version.rb +5 -0
- data/lib/tarsier/websocket.rb +275 -0
- data/lib/tarsier.rb +167 -0
- data/sig/tarsier.rbs +485 -0
- metadata +230 -0
data/README.md
ADDED
|
@@ -0,0 +1,984 @@
|
|
|
1
|
+
# Tarsier
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="https://raw.githubusercontent.com/tarsier-rb/tarsier/main/assets/logo.png" alt="Tarsier Logo" width="200">
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
A modern, high-performance Ruby web framework designed for speed, simplicity, and developer productivity.
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://www.ruby-lang.org/"><img src="https://img.shields.io/badge/ruby-%3E%3D%203.3-red.svg" alt="Ruby"></a>
|
|
13
|
+
<a href="LICENSE.txt"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
Tarsier delivers sub-millisecond routing through a compiled radix tree architecture while maintaining an intuitive API that feels natural to Ruby developers.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Table of Contents
|
|
23
|
+
|
|
24
|
+
- [Features](#features)
|
|
25
|
+
- [Requirements](#requirements)
|
|
26
|
+
- [Installation](#installation)
|
|
27
|
+
- [Quick Start](#quick-start)
|
|
28
|
+
- [Command Line Interface](#command-line-interface)
|
|
29
|
+
- [Routing](#routing)
|
|
30
|
+
- [Controllers](#controllers)
|
|
31
|
+
- [Request and Response](#request-and-response)
|
|
32
|
+
- [Middleware](#middleware)
|
|
33
|
+
- [Database](#database)
|
|
34
|
+
- [WebSocket and SSE](#websocket-and-sse)
|
|
35
|
+
- [Models](#models)
|
|
36
|
+
- [Configuration](#configuration)
|
|
37
|
+
- [Testing](#testing)
|
|
38
|
+
- [Benchmarks](#benchmarks)
|
|
39
|
+
- [Contributing](#contributing)
|
|
40
|
+
- [License](#license)
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Features
|
|
45
|
+
|
|
46
|
+
- **High Performance**: Compiled radix tree router achieving 700,000+ routes per second
|
|
47
|
+
- **Zero Runtime Dependencies**: Core framework operates without external dependencies
|
|
48
|
+
- **Multi-Database Support**: Built-in support for SQLite, PostgreSQL, and MySQL
|
|
49
|
+
- **Modern Ruby**: Built for Ruby 3.3+ with YJIT and Fiber scheduler support
|
|
50
|
+
- **Async Native**: First-class support for Fiber-based concurrency patterns
|
|
51
|
+
- **Type Safety Ready**: Complete RBS type signatures for static analysis
|
|
52
|
+
- **Developer Experience**: Intuitive APIs, detailed error messages, minimal boilerplate
|
|
53
|
+
- **Production Ready**: Built-in middleware for CORS, CSRF, rate limiting, and compression
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Requirements
|
|
58
|
+
|
|
59
|
+
- Ruby 3.3 or higher
|
|
60
|
+
- Rack 3.0+ (for Rack compatibility layer)
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Installation
|
|
65
|
+
|
|
66
|
+
Add Tarsier to your application's Gemfile:
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
gem 'tarsier'
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Then execute:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
bundle install
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Or install globally for CLI access:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
gem install tarsier
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Quick Start
|
|
87
|
+
|
|
88
|
+
Create a new application:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
tarsier new my_app
|
|
92
|
+
cd my_app
|
|
93
|
+
bin/server
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Or create a minimal application manually:
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
# app.rb
|
|
100
|
+
require 'tarsier'
|
|
101
|
+
|
|
102
|
+
# Flask-style minimal syntax
|
|
103
|
+
app = Tarsier.app do
|
|
104
|
+
get('/') { { message: 'Hello, World!' } }
|
|
105
|
+
|
|
106
|
+
get('/users/:id') do |req|
|
|
107
|
+
{ user_id: req.params[:id] }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
post('/users') do |req, res|
|
|
111
|
+
res.status = 201
|
|
112
|
+
{ created: true, name: req.params[:name] }
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
run app
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Start the server:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
rackup app.rb
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Visit `http://localhost:9292` to see your application running.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Command Line Interface
|
|
130
|
+
|
|
131
|
+
Tarsier provides a comprehensive CLI for project scaffolding and development workflows.
|
|
132
|
+
|
|
133
|
+
### Creating Applications
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
# Standard application with views
|
|
137
|
+
tarsier new my_app
|
|
138
|
+
|
|
139
|
+
# API-only application (no view layer)
|
|
140
|
+
tarsier new my_api --api
|
|
141
|
+
|
|
142
|
+
# Minimal application structure
|
|
143
|
+
tarsier new my_app --minimal
|
|
144
|
+
|
|
145
|
+
# Skip optional setup steps
|
|
146
|
+
tarsier new my_app --skip-git --skip-bundle
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Code Generation
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
# Generate a controller with actions
|
|
153
|
+
tarsier generate controller users index show create update destroy
|
|
154
|
+
|
|
155
|
+
# Generate a model with attributes
|
|
156
|
+
tarsier generate model user name:string email:string created_at:datetime
|
|
157
|
+
|
|
158
|
+
# Generate a complete resource (model + controller + routes)
|
|
159
|
+
tarsier generate resource post title:string body:text published:boolean
|
|
160
|
+
|
|
161
|
+
# Generate custom middleware
|
|
162
|
+
tarsier generate middleware authentication
|
|
163
|
+
|
|
164
|
+
# Generate a database migration
|
|
165
|
+
tarsier generate migration create_users name:string email:string
|
|
166
|
+
tarsier generate migration add_role_to_users
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Shorthand aliases are available:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
tarsier g controller users index show
|
|
173
|
+
tarsier g model user name:string
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Development Server
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
# Start with defaults (port 7827)
|
|
180
|
+
tarsier server
|
|
181
|
+
|
|
182
|
+
# Custom port
|
|
183
|
+
tarsier server -p 4000
|
|
184
|
+
|
|
185
|
+
# Specify environment
|
|
186
|
+
tarsier server -e production
|
|
187
|
+
|
|
188
|
+
# Bind to specific host
|
|
189
|
+
tarsier server -b 127.0.0.1
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Interactive Console
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
# Development console
|
|
196
|
+
tarsier console
|
|
197
|
+
|
|
198
|
+
# Production console
|
|
199
|
+
tarsier console -e production
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Route Inspection
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
# Display all routes
|
|
206
|
+
tarsier routes
|
|
207
|
+
|
|
208
|
+
# Filter routes by pattern
|
|
209
|
+
tarsier routes -g users
|
|
210
|
+
|
|
211
|
+
# Output as JSON
|
|
212
|
+
tarsier routes --json
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Version Information
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
tarsier version
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Routing
|
|
224
|
+
|
|
225
|
+
Tarsier uses a compiled radix tree router optimized for high-throughput applications.
|
|
226
|
+
|
|
227
|
+
### Basic Routes
|
|
228
|
+
|
|
229
|
+
```ruby
|
|
230
|
+
Tarsier.routes do
|
|
231
|
+
get '/users', to: 'users#index'
|
|
232
|
+
post '/users', to: 'users#create'
|
|
233
|
+
get '/users/:id', to: 'users#show'
|
|
234
|
+
put '/users/:id', to: 'users#update'
|
|
235
|
+
delete '/users/:id', to: 'users#destroy'
|
|
236
|
+
end
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Route Parameters
|
|
240
|
+
|
|
241
|
+
```ruby
|
|
242
|
+
Tarsier.routes do
|
|
243
|
+
# Required parameters
|
|
244
|
+
get '/users/:id', to: 'users#show'
|
|
245
|
+
|
|
246
|
+
# Multiple parameters
|
|
247
|
+
get '/posts/:post_id/comments/:id', to: 'comments#show'
|
|
248
|
+
|
|
249
|
+
# Wildcard parameters
|
|
250
|
+
get '/files/*path', to: 'files#show'
|
|
251
|
+
|
|
252
|
+
# Parameter constraints
|
|
253
|
+
get '/posts/:id', to: 'posts#show', constraints: { id: /\d+/ }
|
|
254
|
+
end
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### RESTful Resources
|
|
258
|
+
|
|
259
|
+
```ruby
|
|
260
|
+
Tarsier.routes do
|
|
261
|
+
# Full resource routes
|
|
262
|
+
resources :posts
|
|
263
|
+
|
|
264
|
+
# Nested resources
|
|
265
|
+
resources :posts do
|
|
266
|
+
resources :comments
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Limit actions
|
|
270
|
+
resources :posts, only: [:index, :show]
|
|
271
|
+
resources :posts, except: [:destroy]
|
|
272
|
+
|
|
273
|
+
# Singular resource
|
|
274
|
+
resource :profile
|
|
275
|
+
end
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Namespaces and Scopes
|
|
279
|
+
|
|
280
|
+
```ruby
|
|
281
|
+
Tarsier.routes do
|
|
282
|
+
# Namespace (affects path and controller lookup)
|
|
283
|
+
namespace :admin do
|
|
284
|
+
resources :users
|
|
285
|
+
end
|
|
286
|
+
# Routes: /admin/users -> Admin::UsersController
|
|
287
|
+
|
|
288
|
+
# Scope (affects path only)
|
|
289
|
+
scope path: 'api/v1' do
|
|
290
|
+
resources :users
|
|
291
|
+
end
|
|
292
|
+
# Routes: /api/v1/users -> UsersController
|
|
293
|
+
end
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Named Routes
|
|
297
|
+
|
|
298
|
+
```ruby
|
|
299
|
+
Tarsier.routes do
|
|
300
|
+
get '/login', to: 'sessions#new', as: :login
|
|
301
|
+
get '/logout', to: 'sessions#destroy', as: :logout
|
|
302
|
+
|
|
303
|
+
root to: 'home#index'
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# Generate paths
|
|
307
|
+
app.path_for(:login) # => "/login"
|
|
308
|
+
app.path_for(:user, id: 123) # => "/users/123"
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## Controllers
|
|
314
|
+
|
|
315
|
+
Controllers handle request processing with support for filters, parameter validation, and multiple response formats.
|
|
316
|
+
|
|
317
|
+
### Basic Controller
|
|
318
|
+
|
|
319
|
+
```ruby
|
|
320
|
+
class UsersController < Tarsier::Controller
|
|
321
|
+
def index
|
|
322
|
+
users = User.all
|
|
323
|
+
render json: users
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def show
|
|
327
|
+
user = User.find(params[:id])
|
|
328
|
+
render json: user
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def create
|
|
332
|
+
user = User.create(params.permit(:name, :email))
|
|
333
|
+
render json: user, status: 201
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Filters
|
|
339
|
+
|
|
340
|
+
```ruby
|
|
341
|
+
class UsersController < Tarsier::Controller
|
|
342
|
+
before_action :authenticate
|
|
343
|
+
before_action :load_user, only: [:show, :update, :destroy]
|
|
344
|
+
after_action :log_request
|
|
345
|
+
|
|
346
|
+
def show
|
|
347
|
+
render json: @user
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
private
|
|
351
|
+
|
|
352
|
+
def authenticate
|
|
353
|
+
head 401 unless current_user
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def load_user
|
|
357
|
+
@user = User.find(params[:id])
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def log_request
|
|
361
|
+
Logger.info("Processed #{action_name}")
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Parameter Validation
|
|
367
|
+
|
|
368
|
+
```ruby
|
|
369
|
+
class UsersController < Tarsier::Controller
|
|
370
|
+
params do
|
|
371
|
+
requires :name, type: String, min: 2, max: 100
|
|
372
|
+
requires :email, type: String, format: URI::MailTo::EMAIL_REGEXP
|
|
373
|
+
optional :role, type: String, in: %w[user admin], default: 'user'
|
|
374
|
+
optional :age, type: Integer, greater_than: 0
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def create
|
|
378
|
+
# validated_params contains coerced and validated parameters
|
|
379
|
+
user = User.create(validated_params)
|
|
380
|
+
render json: user, status: 201
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Exception Handling
|
|
386
|
+
|
|
387
|
+
```ruby
|
|
388
|
+
class ApplicationController < Tarsier::Controller
|
|
389
|
+
rescue_from ActiveRecord::RecordNotFound, with: :not_found
|
|
390
|
+
rescue_from ValidationError, with: :unprocessable
|
|
391
|
+
|
|
392
|
+
private
|
|
393
|
+
|
|
394
|
+
def not_found(exception)
|
|
395
|
+
render json: { error: 'Resource not found' }, status: 404
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def unprocessable(exception)
|
|
399
|
+
render json: { errors: exception.errors }, status: 422
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### Response Streaming
|
|
405
|
+
|
|
406
|
+
```ruby
|
|
407
|
+
class ReportsController < Tarsier::Controller
|
|
408
|
+
def export
|
|
409
|
+
response.stream do |out|
|
|
410
|
+
User.find_each do |user|
|
|
411
|
+
out << user.to_csv
|
|
412
|
+
out << "\n"
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
---
|
|
420
|
+
|
|
421
|
+
## Request and Response
|
|
422
|
+
|
|
423
|
+
### Request Object
|
|
424
|
+
|
|
425
|
+
The request object provides immutable access to request data with lazy parsing for optimal performance.
|
|
426
|
+
|
|
427
|
+
```ruby
|
|
428
|
+
# HTTP method and path
|
|
429
|
+
request.method # => :GET
|
|
430
|
+
request.path # => '/users/123'
|
|
431
|
+
request.url # => 'https://example.com/users/123'
|
|
432
|
+
|
|
433
|
+
# Parameters
|
|
434
|
+
request.params # Combined route, query, and body params
|
|
435
|
+
request.query_params # Query string parameters only
|
|
436
|
+
request.body_params # Parsed body parameters
|
|
437
|
+
request.route_params # Parameters from route matching
|
|
438
|
+
|
|
439
|
+
# Headers and metadata
|
|
440
|
+
request.headers # All HTTP headers
|
|
441
|
+
request.header('Authorization') # Specific header
|
|
442
|
+
request.content_type # Content-Type header
|
|
443
|
+
request.accept # Accept header
|
|
444
|
+
request.user_agent # User-Agent header
|
|
445
|
+
|
|
446
|
+
# Client information
|
|
447
|
+
request.ip # Client IP address
|
|
448
|
+
request.cookies # Parsed cookies
|
|
449
|
+
|
|
450
|
+
# Request type checks
|
|
451
|
+
request.get? # => true/false
|
|
452
|
+
request.post? # => true/false
|
|
453
|
+
request.json? # Content-Type includes application/json
|
|
454
|
+
request.xhr? # X-Requested-With: XMLHttpRequest
|
|
455
|
+
request.secure? # HTTPS request
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
### Response Object
|
|
459
|
+
|
|
460
|
+
```ruby
|
|
461
|
+
# Set status and headers
|
|
462
|
+
response.status = 201
|
|
463
|
+
response.set_header('X-Custom-Header', 'value')
|
|
464
|
+
response.content_type = 'application/json'
|
|
465
|
+
|
|
466
|
+
# Response helpers
|
|
467
|
+
response.json({ data: users })
|
|
468
|
+
response.html('<h1>Hello</h1>')
|
|
469
|
+
response.text('Plain text response')
|
|
470
|
+
response.redirect('/new-location', status: 302)
|
|
471
|
+
|
|
472
|
+
# Cookies
|
|
473
|
+
response.set_cookie('session', token,
|
|
474
|
+
http_only: true,
|
|
475
|
+
secure: true,
|
|
476
|
+
same_site: 'Strict',
|
|
477
|
+
max_age: 86400
|
|
478
|
+
)
|
|
479
|
+
response.delete_cookie('session')
|
|
480
|
+
|
|
481
|
+
# Streaming
|
|
482
|
+
response.stream do |out|
|
|
483
|
+
out << 'chunk 1'
|
|
484
|
+
out << 'chunk 2'
|
|
485
|
+
end
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
---
|
|
489
|
+
|
|
490
|
+
## Middleware
|
|
491
|
+
|
|
492
|
+
Tarsier includes production-ready middleware and supports custom middleware development.
|
|
493
|
+
|
|
494
|
+
### Built-in Middleware
|
|
495
|
+
|
|
496
|
+
```ruby
|
|
497
|
+
app = Tarsier.application do
|
|
498
|
+
# Request logging
|
|
499
|
+
use Tarsier::Middleware::Logger
|
|
500
|
+
|
|
501
|
+
# CORS handling
|
|
502
|
+
use Tarsier::Middleware::CORS,
|
|
503
|
+
origins: ['https://example.com'],
|
|
504
|
+
methods: %w[GET POST PUT DELETE],
|
|
505
|
+
credentials: true
|
|
506
|
+
|
|
507
|
+
# CSRF protection
|
|
508
|
+
use Tarsier::Middleware::CSRF,
|
|
509
|
+
skip: ['/api/webhooks']
|
|
510
|
+
|
|
511
|
+
# Rate limiting
|
|
512
|
+
use Tarsier::Middleware::RateLimit,
|
|
513
|
+
limit: 100,
|
|
514
|
+
window: 60
|
|
515
|
+
|
|
516
|
+
# Response compression
|
|
517
|
+
use Tarsier::Middleware::Compression,
|
|
518
|
+
min_size: 1024
|
|
519
|
+
|
|
520
|
+
# Static file serving
|
|
521
|
+
use Tarsier::Middleware::Static,
|
|
522
|
+
root: 'public',
|
|
523
|
+
cache_control: 'public, max-age=31536000'
|
|
524
|
+
end
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
### Custom Middleware
|
|
528
|
+
|
|
529
|
+
```ruby
|
|
530
|
+
class TimingMiddleware < Tarsier::Middleware::Base
|
|
531
|
+
def call(request, response)
|
|
532
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
533
|
+
|
|
534
|
+
@app.call(request, response)
|
|
535
|
+
|
|
536
|
+
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
|
537
|
+
response.set_header('X-Response-Time', "#{(duration * 1000).round(2)}ms")
|
|
538
|
+
|
|
539
|
+
response
|
|
540
|
+
end
|
|
541
|
+
end
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
### Middleware Stack Management
|
|
545
|
+
|
|
546
|
+
```ruby
|
|
547
|
+
app.middleware_stack.insert_before(Tarsier::Middleware::Logger, CustomMiddleware)
|
|
548
|
+
app.middleware_stack.insert_after(Tarsier::Middleware::CORS, AnotherMiddleware)
|
|
549
|
+
app.middleware_stack.delete(Tarsier::Middleware::Static)
|
|
550
|
+
app.middleware_stack.swap(OldMiddleware, NewMiddleware)
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
---
|
|
554
|
+
|
|
555
|
+
## Database
|
|
556
|
+
|
|
557
|
+
Tarsier includes a lightweight database layer with support for SQLite, PostgreSQL, and MySQL.
|
|
558
|
+
|
|
559
|
+
### Connecting to a Database
|
|
560
|
+
|
|
561
|
+
```ruby
|
|
562
|
+
# SQLite
|
|
563
|
+
Tarsier.db :sqlite, 'db/app.db'
|
|
564
|
+
Tarsier.db :sqlite, ':memory:' # In-memory database
|
|
565
|
+
|
|
566
|
+
# PostgreSQL
|
|
567
|
+
Tarsier.db :postgres, 'postgres://localhost/myapp'
|
|
568
|
+
Tarsier.db :postgres, host: 'localhost', database: 'myapp', username: 'user'
|
|
569
|
+
|
|
570
|
+
# MySQL
|
|
571
|
+
Tarsier.db :mysql, host: 'localhost', database: 'myapp', username: 'root'
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
### Raw Queries
|
|
575
|
+
|
|
576
|
+
```ruby
|
|
577
|
+
# Execute queries
|
|
578
|
+
Tarsier.database.execute('SELECT * FROM users WHERE active = ?', true)
|
|
579
|
+
|
|
580
|
+
# Get single result
|
|
581
|
+
user = Tarsier.database.get('SELECT * FROM users WHERE id = ?', 1)
|
|
582
|
+
|
|
583
|
+
# Insert and get ID
|
|
584
|
+
id = Tarsier.database.insert(:users, name: 'John', email: 'john@example.com')
|
|
585
|
+
|
|
586
|
+
# Update records
|
|
587
|
+
Tarsier.database.update(:users, { active: false }, id: 1)
|
|
588
|
+
|
|
589
|
+
# Delete records
|
|
590
|
+
Tarsier.database.delete(:users, id: 1)
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
### Transactions
|
|
594
|
+
|
|
595
|
+
```ruby
|
|
596
|
+
Tarsier.database.transaction do
|
|
597
|
+
Tarsier.database.insert(:accounts, user_id: 1, balance: 100)
|
|
598
|
+
Tarsier.database.update(:users, { has_account: true }, id: 1)
|
|
599
|
+
end
|
|
600
|
+
# Automatically rolls back on error
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
### Migrations
|
|
604
|
+
|
|
605
|
+
Generate a migration:
|
|
606
|
+
|
|
607
|
+
```bash
|
|
608
|
+
tarsier generate migration create_users name:string email:string
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
Migration file:
|
|
612
|
+
|
|
613
|
+
```ruby
|
|
614
|
+
class CreateUsers < Tarsier::Database::Migration
|
|
615
|
+
def up
|
|
616
|
+
create_table :users do |t|
|
|
617
|
+
t.string :name
|
|
618
|
+
t.string :email, null: false
|
|
619
|
+
t.boolean :active, default: true
|
|
620
|
+
t.timestamps
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
add_index :users, :email, unique: true
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
def down
|
|
627
|
+
drop_table :users
|
|
628
|
+
end
|
|
629
|
+
end
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
Run migrations:
|
|
633
|
+
|
|
634
|
+
```ruby
|
|
635
|
+
Tarsier.database.migrate # Run pending migrations
|
|
636
|
+
Tarsier.database.migrate(:down) # Rollback migrations
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
### Supported Column Types
|
|
640
|
+
|
|
641
|
+
| Type | SQL Type |
|
|
642
|
+
|------|----------|
|
|
643
|
+
| string | VARCHAR(255) |
|
|
644
|
+
| text | TEXT |
|
|
645
|
+
| integer | INTEGER |
|
|
646
|
+
| bigint | BIGINT |
|
|
647
|
+
| float | REAL |
|
|
648
|
+
| decimal | DECIMAL |
|
|
649
|
+
| boolean | BOOLEAN |
|
|
650
|
+
| date | DATE |
|
|
651
|
+
| datetime | DATETIME |
|
|
652
|
+
| timestamp | TIMESTAMP |
|
|
653
|
+
| time | TIME |
|
|
654
|
+
| binary | BLOB |
|
|
655
|
+
| json | JSON |
|
|
656
|
+
|
|
657
|
+
---
|
|
658
|
+
|
|
659
|
+
## WebSocket and SSE
|
|
660
|
+
|
|
661
|
+
### WebSocket Support
|
|
662
|
+
|
|
663
|
+
```ruby
|
|
664
|
+
class ChatSocket < Tarsier::WebSocket
|
|
665
|
+
on :connect do
|
|
666
|
+
subscribe "room:#{params[:room_id]}"
|
|
667
|
+
broadcast "room:#{params[:room_id]}", { type: 'join', user: current_user }
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
on :message do |data|
|
|
671
|
+
broadcast "room:#{params[:room_id]}", {
|
|
672
|
+
type: 'message',
|
|
673
|
+
user: current_user,
|
|
674
|
+
content: data
|
|
675
|
+
}
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
on :close do
|
|
679
|
+
broadcast "room:#{params[:room_id]}", { type: 'leave', user: current_user }
|
|
680
|
+
end
|
|
681
|
+
end
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
### Server-Sent Events
|
|
685
|
+
|
|
686
|
+
```ruby
|
|
687
|
+
class EventsController < Tarsier::Controller
|
|
688
|
+
def stream
|
|
689
|
+
sse = Tarsier::SSE.new(request, response)
|
|
690
|
+
sse.start
|
|
691
|
+
|
|
692
|
+
loop do
|
|
693
|
+
sse.send({ time: Time.now.iso8601 }, event: 'tick', id: SecureRandom.uuid)
|
|
694
|
+
sleep 1
|
|
695
|
+
end
|
|
696
|
+
rescue IOError
|
|
697
|
+
sse.close
|
|
698
|
+
end
|
|
699
|
+
end
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
---
|
|
703
|
+
|
|
704
|
+
## Models
|
|
705
|
+
|
|
706
|
+
Tarsier includes a lightweight ORM with Active Record-style patterns. Models integrate seamlessly with the database layer.
|
|
707
|
+
|
|
708
|
+
### Model Definition
|
|
709
|
+
|
|
710
|
+
```ruby
|
|
711
|
+
class User < Tarsier::Model
|
|
712
|
+
table :users
|
|
713
|
+
|
|
714
|
+
attribute :name, :string
|
|
715
|
+
attribute :email, :string
|
|
716
|
+
attribute :active, :boolean, default: true
|
|
717
|
+
attribute :created_at, :datetime
|
|
718
|
+
|
|
719
|
+
validates :name, presence: true, length: { minimum: 2, maximum: 100 }
|
|
720
|
+
validates :email, presence: true, format: /@/
|
|
721
|
+
|
|
722
|
+
has_many :posts
|
|
723
|
+
has_one :profile
|
|
724
|
+
belongs_to :organization
|
|
725
|
+
end
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
### CRUD Operations
|
|
729
|
+
|
|
730
|
+
```ruby
|
|
731
|
+
# Create
|
|
732
|
+
user = User.create(name: 'John', email: 'john@example.com')
|
|
733
|
+
user = User.new(name: 'Jane')
|
|
734
|
+
user.save
|
|
735
|
+
|
|
736
|
+
# Read
|
|
737
|
+
User.find(1)
|
|
738
|
+
User.find!(1) # Raises RecordNotFoundError if not found
|
|
739
|
+
User.find_by(email: 'john@example.com')
|
|
740
|
+
User.all
|
|
741
|
+
|
|
742
|
+
# Update
|
|
743
|
+
user.update(name: 'John Doe')
|
|
744
|
+
user.name = 'Johnny'
|
|
745
|
+
user.save
|
|
746
|
+
|
|
747
|
+
# Delete
|
|
748
|
+
user.destroy
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
### Query Interface
|
|
752
|
+
|
|
753
|
+
```ruby
|
|
754
|
+
# Chainable query methods
|
|
755
|
+
User.where(active: true)
|
|
756
|
+
.where_not(role: 'guest')
|
|
757
|
+
.order(created_at: :desc)
|
|
758
|
+
.limit(10)
|
|
759
|
+
.offset(20)
|
|
760
|
+
|
|
761
|
+
# Range queries
|
|
762
|
+
User.where(age: 18..65)
|
|
763
|
+
|
|
764
|
+
# IN queries
|
|
765
|
+
User.where(status: ['active', 'pending'])
|
|
766
|
+
|
|
767
|
+
# Select specific columns
|
|
768
|
+
User.select(:id, :name, :email)
|
|
769
|
+
|
|
770
|
+
# Pluck values
|
|
771
|
+
User.where(active: true).pluck(:email)
|
|
772
|
+
User.pluck(:id) # Same as User.ids
|
|
773
|
+
|
|
774
|
+
# Joins
|
|
775
|
+
User.joins(:posts, on: 'users.id = posts.user_id')
|
|
776
|
+
|
|
777
|
+
# Eager loading
|
|
778
|
+
User.includes(:posts, :profile)
|
|
779
|
+
|
|
780
|
+
# Aggregations
|
|
781
|
+
User.count
|
|
782
|
+
User.where(active: true).count
|
|
783
|
+
User.exists?(email: 'test@example.com')
|
|
784
|
+
|
|
785
|
+
# Batch operations
|
|
786
|
+
User.where(inactive: true).update_all(archived: true)
|
|
787
|
+
User.where(archived: true).delete_all
|
|
788
|
+
|
|
789
|
+
# Find or create
|
|
790
|
+
User.where(email: 'new@example.com').find_or_create_by({ email: 'new@example.com' })
|
|
791
|
+
User.where(email: 'new@example.com').find_or_initialize_by({ email: 'new@example.com' })
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
### Validations
|
|
795
|
+
|
|
796
|
+
```ruby
|
|
797
|
+
class Post < Tarsier::Model
|
|
798
|
+
attribute :title, :string
|
|
799
|
+
attribute :body, :text
|
|
800
|
+
attribute :status, :string
|
|
801
|
+
|
|
802
|
+
validates :title, presence: true, length: { minimum: 5, maximum: 200 }
|
|
803
|
+
validates :status, inclusion: { in: %w[draft published archived] }
|
|
804
|
+
end
|
|
805
|
+
|
|
806
|
+
post = Post.new(title: 'Hi')
|
|
807
|
+
post.valid? # => false
|
|
808
|
+
post.errors # => { title: ["is too short (minimum 5)"] }
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
### Associations
|
|
812
|
+
|
|
813
|
+
```ruby
|
|
814
|
+
class Post < Tarsier::Model
|
|
815
|
+
belongs_to :user
|
|
816
|
+
has_many :comments
|
|
817
|
+
end
|
|
818
|
+
|
|
819
|
+
class Comment < Tarsier::Model
|
|
820
|
+
belongs_to :post
|
|
821
|
+
belongs_to :user
|
|
822
|
+
end
|
|
823
|
+
|
|
824
|
+
# Access associations
|
|
825
|
+
post = Post.find(1)
|
|
826
|
+
post.user # => User instance
|
|
827
|
+
post.comments # => Array of Comment instances
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
---
|
|
831
|
+
|
|
832
|
+
## Configuration
|
|
833
|
+
|
|
834
|
+
```ruby
|
|
835
|
+
Tarsier.application do
|
|
836
|
+
configure do
|
|
837
|
+
# Security
|
|
838
|
+
self.secret_key = ENV.fetch('SECRET_KEY')
|
|
839
|
+
self.force_ssl = true
|
|
840
|
+
|
|
841
|
+
# Server
|
|
842
|
+
self.host = '0.0.0.0'
|
|
843
|
+
self.port = 3000
|
|
844
|
+
|
|
845
|
+
# Application
|
|
846
|
+
self.log_level = :info
|
|
847
|
+
self.default_format = :json
|
|
848
|
+
self.static_files = true
|
|
849
|
+
end
|
|
850
|
+
end
|
|
851
|
+
```
|
|
852
|
+
|
|
853
|
+
### Environment-Specific Configuration
|
|
854
|
+
|
|
855
|
+
```ruby
|
|
856
|
+
Tarsier.application do
|
|
857
|
+
configure do
|
|
858
|
+
case Tarsier.env
|
|
859
|
+
when 'production'
|
|
860
|
+
self.force_ssl = true
|
|
861
|
+
self.log_level = :warn
|
|
862
|
+
when 'development'
|
|
863
|
+
self.log_level = :debug
|
|
864
|
+
when 'test'
|
|
865
|
+
self.log_level = :error
|
|
866
|
+
end
|
|
867
|
+
end
|
|
868
|
+
end
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
---
|
|
872
|
+
|
|
873
|
+
## Testing
|
|
874
|
+
|
|
875
|
+
Tarsier applications are designed for testability with RSpec integration.
|
|
876
|
+
|
|
877
|
+
### Setup
|
|
878
|
+
|
|
879
|
+
```ruby
|
|
880
|
+
# spec/spec_helper.rb
|
|
881
|
+
require 'tarsier'
|
|
882
|
+
require 'rack/test'
|
|
883
|
+
|
|
884
|
+
RSpec.configure do |config|
|
|
885
|
+
config.include Rack::Test::Methods
|
|
886
|
+
|
|
887
|
+
def app
|
|
888
|
+
Tarsier.app
|
|
889
|
+
end
|
|
890
|
+
end
|
|
891
|
+
```
|
|
892
|
+
|
|
893
|
+
### Controller Tests
|
|
894
|
+
|
|
895
|
+
```ruby
|
|
896
|
+
RSpec.describe UsersController do
|
|
897
|
+
describe 'GET /users' do
|
|
898
|
+
it 'returns users list' do
|
|
899
|
+
get '/users'
|
|
900
|
+
|
|
901
|
+
expect(last_response.status).to eq(200)
|
|
902
|
+
expect(JSON.parse(last_response.body)).to have_key('users')
|
|
903
|
+
end
|
|
904
|
+
end
|
|
905
|
+
|
|
906
|
+
describe 'POST /users' do
|
|
907
|
+
it 'creates a user' do
|
|
908
|
+
post '/users', { name: 'John', email: 'john@example.com' }.to_json,
|
|
909
|
+
'CONTENT_TYPE' => 'application/json'
|
|
910
|
+
|
|
911
|
+
expect(last_response.status).to eq(201)
|
|
912
|
+
end
|
|
913
|
+
end
|
|
914
|
+
end
|
|
915
|
+
```
|
|
916
|
+
|
|
917
|
+
### Running Tests
|
|
918
|
+
|
|
919
|
+
```bash
|
|
920
|
+
bundle exec rspec
|
|
921
|
+
bundle exec rspec spec/controllers
|
|
922
|
+
bundle exec rspec --format documentation
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
---
|
|
926
|
+
|
|
927
|
+
## Benchmarks
|
|
928
|
+
|
|
929
|
+
Run the included benchmark suite:
|
|
930
|
+
|
|
931
|
+
```bash
|
|
932
|
+
bundle exec rake benchmark
|
|
933
|
+
bundle exec rake benchmark:router
|
|
934
|
+
bundle exec rake benchmark:request
|
|
935
|
+
```
|
|
936
|
+
|
|
937
|
+
### Performance Characteristics
|
|
938
|
+
|
|
939
|
+
| Operation | Throughput |
|
|
940
|
+
|-----------|------------|
|
|
941
|
+
| Static route matching | 700,000+ req/sec |
|
|
942
|
+
| Parameterized routes | 250,000+ req/sec |
|
|
943
|
+
| Nested resources | 115,000+ req/sec |
|
|
944
|
+
| Request parsing | 700,000+ req/sec |
|
|
945
|
+
|
|
946
|
+
Benchmarks performed on Ruby 3.3 with YJIT enabled.
|
|
947
|
+
|
|
948
|
+
---
|
|
949
|
+
|
|
950
|
+
## Contributing
|
|
951
|
+
|
|
952
|
+
1. Fork the repository
|
|
953
|
+
2. Create a feature branch (`git checkout -b feature/improvement`)
|
|
954
|
+
3. Commit your changes (`git commit -am 'Add new feature'`)
|
|
955
|
+
4. Push to the branch (`git push origin feature/improvement`)
|
|
956
|
+
5. Create a Pull Request
|
|
957
|
+
|
|
958
|
+
### Development Setup
|
|
959
|
+
|
|
960
|
+
```bash
|
|
961
|
+
git clone https://github.com/tarsier-rb/tarsier.git
|
|
962
|
+
cd tarsier
|
|
963
|
+
bundle install
|
|
964
|
+
bundle exec rspec
|
|
965
|
+
```
|
|
966
|
+
|
|
967
|
+
### Code Quality
|
|
968
|
+
|
|
969
|
+
```bash
|
|
970
|
+
bundle exec rubocop
|
|
971
|
+
bundle exec yard doc
|
|
972
|
+
```
|
|
973
|
+
|
|
974
|
+
---
|
|
975
|
+
|
|
976
|
+
## License
|
|
977
|
+
|
|
978
|
+
Tarsier is released under the [MIT License](LICENSE.txt).
|
|
979
|
+
|
|
980
|
+
---
|
|
981
|
+
|
|
982
|
+
## Acknowledgments
|
|
983
|
+
|
|
984
|
+
Tarsier draws inspiration from the Ruby web framework ecosystem, particularly Rails, Sinatra, and Roda, while pursuing its own vision of performance and simplicity.
|