gr_api_manager 0.1.0 → 0.4.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.
Files changed (5) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +456 -187
  3. data/README_ES.md +615 -0
  4. data/lib/gr_api_manager.rb +766 -59
  5. metadata +72 -8
data/README.md CHANGED
@@ -1,345 +1,614 @@
1
1
  # GR API Manager
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/gr-api-manager.svg)](https://rubygems.org/gems/gr-api-manager)
3
+ [![Gem Version](https://img.shields.io/gem/v/gr_api_manager.svg?logo=rubygems&logoColor=white&color=e9573f)](https://rubygems.org/gems/gr_api_manager)
4
+ [![Gem Total Downloads](https://img.shields.io/gem/dt/gr_api_manager.svg?logo=rubygems&logoColor=white&color=00bfa5)](https://rubygems.org/gems/gr_api_manager)
5
+ [![Ruby Version](https://img.shields.io/badge/Ruby-%3E%3D%203.0-cc342d.svg?logo=ruby&logoColor=white)](https://www.ruby-lang.org/)
6
+ [![Sinatra Version](https://img.shields.io/badge/Sinatra-%3E%3D%203.0-008080.svg?logo=sinatra&logoColor=white)](https://sinatrarb.com/)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-007ec6.svg?logo=open-source-initiative&logoColor=white)](LICENSE)
8
+ [![Platform](https://img.shields.io/badge/Platform-Linux%20%7C%20macOS%20%7C%20Windows-555555.svg?logo=linux&logoColor=white)](https://rubygems.org/gems/gr_api_manager)
9
+ [![Tests](https://img.shields.io/badge/Tests-59%2F59%20Passing-4c1.svg?logo=checkmarx&logoColor=white)](spec/)
10
+ [![README en Español](https://img.shields.io/badge/README-Espa%C3%B1ol-red.svg?logo=google-translate&logoColor=white)](README_ES.md)
4
11
 
5
- A minimal, opinionated Ruby wrapper around Sinatra that eliminates boilerplate from REST API development. Authentication, parameter validation, type casting, CORS, and error handling are handled at the framework level you write only the business logic.
12
+ **GR API Manager** is a minimalist, high-performance Ruby wrapper on top of Sinatra and Puma, designed to build production-grade REST APIs with zero boilerplate.
6
13
 
7
14
  ---
8
15
 
9
- ## Features
16
+ ## System Requirements
10
17
 
11
- - **Available on RubyGems!** Install globally and use it in any project.
12
- - Route-level Bearer Token authentication (opt-in/opt-out per endpoint).
13
- - Declarative required parameter validation with automatic `400 Bad Request` responses.
14
- - Smart type casting for query string values (`"10"` -> `Integer`, `"true"` -> `TrueClass`, `"9.5"` -> `Float`).
15
- - Global JSON error responses for `404` and `500`.
16
- - Broad CORS and `OPTIONS` preflight handling out of the box (Frontend ready).
17
- - API versioning via configurable route prefix (e.g. `/api/v1`).
18
+ | Dependency / Environment | Required / Supported Version |
19
+ |---|---|
20
+ | **Ruby** | `>= 3.0` (Tested on 3.0, 3.1, 3.2, 3.3, and 3.4) |
21
+ | **Sinatra** | `>= 3.0, < 5.0` |
22
+ | **Puma** | `>= 5.0, < 9.0` |
23
+ | **Dotenv** | `>= 2.8, < 4.0` |
18
24
 
19
25
  ---
20
26
 
21
- ## Installation
27
+ ## Key Features
28
+
29
+ - **Available on RubyGems (`0.4.0`)** - global installation or via Bundler.
30
+ - **Modular Route Groups (`api.group`)** - organize large projects into clean, multi-file sub-routers with prefix & option inheritance.
31
+ - **Dual Authentication (Static Bearer Token & Native JWT)** - support for pre-shared static tokens and zero-dependency built-in HMAC-SHA256 JWT engine.
32
+ - **Declarative Schema & Type Validation** - validate complex request contracts with `:email`, `:url`, `:boolean`, `:file`, classes (`Integer`, `Float`, `String`, `Array`, `Hash`), enum lists, regular expressions, or custom procs.
33
+ - **Sliding-Window Rate Limiting (429)** - sliding-window rate limiter with automatic client IP detection behind Cloudflare (`CF-Connecting-IP`), Nginx (`X-Real-IP`), or Reverse Proxies (`X-Forwarded-For`), supporting in-memory or pluggable stores (Redis).
34
+ - **Smart Parameter Casting** - automatic type casting for query and URL parameters (`"100"` -> `100`, `"-42"` -> `-42`, `"true"` -> `true`, `"19.99"` -> `19.99`).
35
+ - **Unified File & Binary Handling (`FilePayload`)** - transparent handling for multipart uploads, raw binary streams, Base64/Hex encoding, file serving/downloads, and disk storage with directory auto-creation.
36
+ - **Puma Concurrency** - configurable multi-process workers and thread pools.
37
+ - **Developer Mode (`dev_mode`)** - structured JSON error responses with full stack traces for painless debugging.
22
38
 
23
- Add this line to your application's Gemfile:
39
+ ---
40
+
41
+ ## Installation
24
42
 
43
+ Add the gem to your `Gemfile`:
25
44
  ```ruby
26
- gem 'gr-api-manager'
45
+ gem 'gr_api_manager', '~> 0.4.0'
27
46
  ```
28
47
 
29
- And then execute:
48
+ And run:
30
49
  ```bash
31
50
  bundle install
32
51
  ```
33
52
 
34
- Or install it yourself directly via RubyGems:
53
+ Or install directly:
35
54
  ```bash
36
- gem install gr-api-manager
55
+ gem install gr_api_manager
37
56
  ```
38
57
 
39
58
  ---
40
59
 
41
- ## Quick Start
60
+ ## Table of Contents
61
+
62
+ 1. [Quickstart](#quickstart)
63
+ 2. [Authentication: Static Bearer Token & Native JWT](#authentication-static-bearer-token--native-jwt)
64
+ 3. [Multi-File Modular Architecture (Importing & Grouping)](#multi-file-modular-architecture)
65
+ 4. [Full REST CRUD with HTTP Verbs](#full-rest-crud-with-http-verbs)
66
+ 5. [Declarative Schema & Type Validation](#declarative-schema--type-validation)
67
+ 6. [Comprehensive File, Binary & Image Handling](#comprehensive-file-binary--image-handling)
68
+ 7. [Serving & Downloading Files to Clients](#serving--downloading-files-to-clients)
69
+ 8. [Rate Limiting & Real IP Detection (Cloudflare/Nginx)](#rate-limiting--real-ip-detection)
70
+ 9. [Smart Parameter Casting](#smart-parameter-casting)
71
+ 10. [Responses, HTTP Status Codes & Dev Mode](#responses-http-status-codes--dev-mode)
72
+ 11. [Concurrency & Production Deployment (Puma & Docker)](#concurrency--production-deployment)
73
+
74
+ ---
75
+
76
+ ## Quickstart
77
+
78
+ Create an `app.rb` file:
42
79
 
43
80
  ```ruby
44
81
  require 'gr_api_manager'
45
82
 
83
+ # Initialize the server with static token and JWT key
46
84
  api = GRApiManager::Server.new(
47
- port: 4567,
48
- bearer_token: "your_secret_token",
49
- prefix: "/api/v1"
85
+ port: 4000,
86
+ bearer_token: "my_static_secret_token",
87
+ jwt_secret: "my_jwt_signing_secret"
50
88
  )
51
89
 
52
- # Public endpoint
90
+ # Public endpoint (no auth required)
53
91
  api.get('/health', auth: false) do
54
- { status: 'online', time: Time.now.to_s }
92
+ { status: 'online', timestamp: Time.now.to_i }
55
93
  end
56
94
 
95
+ # Protected endpoint with static token (default: auth = true)
96
+ api.get('/protected-data') do
97
+ { message: "Access granted with Bearer Token", data: [10, 20, 30] }
98
+ end
99
+
100
+ # Start the server
57
101
  api.run!
58
102
  ```
59
103
 
104
+ Run your API:
60
105
  ```bash
61
- curl http://localhost:4567/api/v1/health
62
- # => {"status":"online","time":"..."}
106
+ ruby app.rb
63
107
  ```
64
108
 
65
109
  ---
66
110
 
67
- ## Configuration
111
+ ## Authentication: Static Bearer Token & Native JWT
112
+
113
+ `gr_api_manager` supports two complementary authentication schemes:
68
114
 
69
- All parameters are optional. If omitted, the manager falls back to environment variables loaded from a `.env` file at the project root (via `dotenv`). If neither is provided, defaults are used.
115
+ ### 1. Static Bearer Token
70
116
 
117
+ Ideal for internal services, webhooks, microservices, or backend automation using a pre-shared static key.
118
+
119
+ #### Configuration in `app.rb`:
71
120
  ```ruby
72
- GRApiManager::Server.new(
73
- port: 4567, # Default: ENV['PORT'] || 4000
74
- bearer_token: "secret", # Default: ENV['API_TOKEN']
75
- permitted_hosts: ["example.com"], # Host allowlist — empty means allow all
76
- prefix: "/api/v1" # Route prefix applied to all endpoints
121
+ api = GRApiManager::Server.new(
122
+ bearer_token: "my_secret_company_token_2026"
77
123
  )
78
- ```
79
124
 
80
- ### Using a .env file (Recommended)
125
+ # Public route
126
+ api.get('/public', auth: false) do
127
+ { status: "open access" }
128
+ end
81
129
 
82
- Create a `.env` file at the root of your project:
130
+ # Protected routes (default auth: true)
131
+ api.get('/admin/config') do
132
+ { database: "connected", environment: "production" }
133
+ end
83
134
 
84
- ```env
85
- PORT=4567
86
- API_TOKEN=your_secret_token
135
+ api.post('/admin/restart', requires: [:reason]) do |params|
136
+ { action: "restarting", reason: params[:reason] }
137
+ end
87
138
  ```
88
139
 
89
- Then initialize the manager with no arguments and it will pick everything up automatically:
140
+ #### Consuming via cURL:
90
141
 
91
- ```ruby
92
- require 'gr_api_manager'
93
-
94
- api = GRApiManager::Server.new
95
- # Reads PORT and API_TOKEN from .env
142
+ **Valid request (200 OK):**
143
+ ```bash
144
+ curl -H "Authorization: Bearer my_secret_company_token_2026" http://localhost:4000/admin/config
145
+ # => {"database":"connected","environment":"production"}
96
146
  ```
97
147
 
98
- This is the recommended approach for production — keep secrets out of source code and out of version control. Add `.env` to your `.gitignore`.
148
+ **Missing Authorization header (401 Unauthorized):**
149
+ ```bash
150
+ curl http://localhost:4000/admin/config
151
+ # => 401 {"error":"Token required. Format: 'Bearer <token>'"}
152
+ ```
99
153
 
100
- ```text
101
- # .gitignore
102
- .env
154
+ **Invalid token (403 Forbidden):**
155
+ ```bash
156
+ curl -H "Authorization: Bearer wrong_token" http://localhost:4000/admin/config
157
+ # => 403 {"error":"Invalid token"}
103
158
  ```
104
159
 
105
- ### Priority order
160
+ ---
161
+
162
+ ### 2. Native JWT Authentication (HS256)
106
163
 
107
- When a value is provided both in code and in `.env`, the explicit argument always wins:
164
+ Ideal for end-user facing APIs issuing dynamic tokens with expiration and claims.
108
165
 
109
- ```text
110
- new(bearer_token: "hardcoded") > ENV['API_TOKEN'] > nil (auth disabled)
111
- new(port: 4567) > ENV['PORT'] > 4000
166
+ #### Complete Workflow:
167
+ ```ruby
168
+ api = GRApiManager::Server.new(
169
+ jwt_secret: "my_super_secret_jwt_key"
170
+ )
171
+
172
+ # 1. Login: issue and return token
173
+ api.post('/auth/login', auth: false, requires: { email: :email, password: String }) do |params|
174
+ if params[:email] == "admin@company.com" && params[:password] == "pass123"
175
+ token = api.jwt_encode(
176
+ { user_id: 42, email: params[:email], role: "admin" },
177
+ exp: Time.now.to_i + 3600 # 1-hour expiration
178
+ )
179
+ { token: token, token_type: "Bearer", expires_in: 3600 }
180
+ else
181
+ status 401
182
+ { error: "Invalid credentials" }
183
+ end
184
+ end
185
+
186
+ # 2. JWT-protected route: automatically injects params[:current_user]
187
+ api.get('/profile', auth: :jwt) do |params|
188
+ user = params[:current_user]
189
+ {
190
+ message: "Valid JWT token",
191
+ user_id: user[:user_id],
192
+ email: user[:email],
193
+ role: user[:role]
194
+ }
195
+ end
112
196
  ```
113
197
 
114
198
  ---
115
199
 
116
- ## Defining Routes
200
+ ## Multi-File Modular Architecture
201
+
202
+ Divide your API into dedicated, clean route modules within a `routes/` directory:
117
203
 
118
- The manager exposes `get`, `post`, `put`, and `delete` methods. Each route receives a merged `params` hash containing both URL/query parameters (type-cast) and the parsed JSON body.
204
+ ```text
205
+ my_api_project/
206
+ ├── app.rb # Main entrypoint and configuration
207
+ ├── .env # Environment variables
208
+ ├── Gemfile
209
+ └── routes/
210
+ ├── auth_routes.rb # Login & registration
211
+ ├── admin_routes.rb # Admin dashboard
212
+ ├── payments_routes.rb # Checkout & billing
213
+ └── files_routes.rb # File uploads & storage
214
+ ```
119
215
 
216
+ ### Module 1: `routes/auth_routes.rb`
120
217
  ```ruby
121
- api.get('/users', auth: true, requires: [:role, :age]) do |params|
122
- # params is a merged, type-cast hash of all inputs
123
- {
124
- requested_role: params[:role],
125
- is_adult: params[:age] >= 18 # age is safely cast to Integer
126
- }
218
+ module AuthRoutes
219
+ def self.setup(router, api_server)
220
+ router.post('/login', auth: false, requires: { email: :email, password: String }) do |params|
221
+ if params[:email] == "admin@company.com" && params[:password] == "secret"
222
+ token = api_server.jwt_encode({ user_id: 1, email: params[:email], role: "admin" })
223
+ { token: token }
224
+ else
225
+ status 401
226
+ { error: "Invalid credentials" }
227
+ end
228
+ end
229
+ end
127
230
  end
128
231
  ```
129
232
 
130
- ### Options
233
+ ### Module 2: `routes/admin_routes.rb`
234
+ ```ruby
235
+ module AdminRoutes
236
+ def self.setup(router)
237
+ router.get('/metrics') do |params|
238
+ { cpu: "12%", memory: "380MB", user: params[:current_user][:email] }
239
+ end
240
+
241
+ router.delete('/users/:id') do |params|
242
+ { message: "User #{params[:id]} deleted" }
243
+ end
244
+ end
245
+ end
246
+ ```
131
247
 
132
- | Option | Type | Default | Description |
133
- |------------|---------|---------|--------------------------------------------------|
134
- | `auth` | Boolean | `true` | Require Bearer Token for this route |
135
- | `requires` | Array | `[]` | List of required parameter keys (symbols/strings)|
248
+ ### Module 3: `routes/files_routes.rb`
249
+ ```ruby
250
+ module FilesRoutes
251
+ def self.setup(router)
252
+ router.post('/upload', requires: [:title]) do |params|
253
+ file = params[:_files][:document]
254
+ path = file.save_to("./storage/#{params[:title]}#{file.extension}")
255
+ { status: "saved", path: path, size: file.size }
256
+ end
257
+ end
258
+ end
259
+ ```
136
260
 
137
- ---
261
+ ### Main Application File: `app.rb`
262
+ ```ruby
263
+ require 'gr_api_manager'
138
264
 
139
- ## Authentication
265
+ # Require modular route files
266
+ require_relative 'routes/auth_routes'
267
+ require_relative 'routes/admin_routes'
268
+ require_relative 'routes/files_routes'
140
269
 
141
- All routes require a valid Bearer Token by default. Pass the token in the `Authorization` header:
270
+ api = GRApiManager::Server.new(
271
+ port: 4000,
272
+ jwt_secret: ENV['JWT_SECRET'] || "default_jwt_secret",
273
+ bearer_token: ENV['API_TOKEN'] || "global_static_token"
274
+ )
142
275
 
143
- ```bash
144
- curl -H "Authorization: Bearer your_secret_token" http://localhost:4567/api/v1/users
276
+ # Root route
277
+ api.get('/', auth: false) { { service: "Core API v1.0" } }
278
+
279
+ # Mount route groups
280
+ api.group('/auth') { |g| AuthRoutes.setup(g, api) }
281
+ api.group('/admin', auth: :jwt) { |g| AdminRoutes.setup(g) }
282
+ api.group('/files', auth: true) { |g| FilesRoutes.setup(g) }
283
+
284
+ api.run!(workers: 2, threads: '2:8')
145
285
  ```
146
286
 
147
- To make a route public, set `auth: false`:
287
+ ---
288
+
289
+ ## Full REST CRUD with HTTP Verbs
290
+
291
+ Example managing a `/products` resource:
148
292
 
149
293
  ```ruby
150
- api.get('/health', auth: false) do
151
- { status: 'online' }
294
+ api = GRApiManager::Server.new(prefix: '/api/v1')
295
+
296
+ # 1. LIST (GET) - with auto-cast query parameters
297
+ api.get('/products', auth: false) do |params|
298
+ page = params[:page] || 1 # Integer
299
+ limit = params[:limit] || 10 # Integer
300
+ active = params[:active] != false # Boolean
301
+
302
+ {
303
+ page: page,
304
+ limit: limit,
305
+ items: [
306
+ { id: 1, name: "Mechanical Keyboard", price: 89.99, active: true },
307
+ { id: 2, name: "4K Monitor", price: 299.99, active: true }
308
+ ]
309
+ }
152
310
  end
153
- ```
154
311
 
155
- **Automatic Error Responses:**
312
+ # 2. GET BY ID (GET)
313
+ api.get('/products/:id', auth: false) do |params|
314
+ id = params[:id] # Auto Integer
315
+ { id: id, name: "Product #{id}", price: 49.99 }
316
+ end
156
317
 
157
- ```json
158
- // Missing header (401)
159
- { "error": "Token required. Format: 'Bearer <token>'" }
318
+ # 3. CREATE (POST) - with strong type validation
319
+ api.post('/products', requires: { name: String, price: Float, category: ['tech', 'office'] }) do |params|
320
+ status 201
321
+ {
322
+ message: "Product created",
323
+ product: { id: rand(100..999), name: params[:name], price: params[:price] }
324
+ }
325
+ end
160
326
 
161
- // Wrong token (403)
162
- { "error": "Invalid token" }
327
+ # 4. FULL UPDATE (PUT)
328
+ api.put('/products/:id', requires: { name: String, price: Float }) do |params|
329
+ {
330
+ message: "Product #{params[:id]} completely updated",
331
+ data: params
332
+ }
333
+ end
334
+
335
+ # 5. PARTIAL UPDATE (PATCH)
336
+ api.patch('/products/:id') do |params|
337
+ {
338
+ message: "Updated fields for product #{params[:id]}",
339
+ changes: params.except(:id)
340
+ }
341
+ end
342
+
343
+ # 6. DELETE (DELETE)
344
+ api.delete('/products/:id') do |params|
345
+ { message: "Product #{params[:id]} deleted successfully" }
346
+ end
163
347
  ```
164
348
 
165
349
  ---
166
350
 
167
- ## Parameter Validation
351
+ ## Declarative Schema & Type Validation
168
352
 
169
- Declare required parameters at the route level. If any are missing or blank, the framework responds with `400 Bad Request` automatically — no conditional logic needed in your handler.
353
+ The `requires:` option validates data types, string formats, uploaded files, and custom logic:
170
354
 
171
355
  ```ruby
172
- api.post('/users', requires: [:name, :email]) do |params|
356
+ api.post '/catalog', requires: {
357
+ sku: /^[A-Z]{3}-\d{4}$/, # Regexp: e.g. "PRO-1234"
358
+ title: String, # Non-empty string
359
+ price: Float, # Float number
360
+ stock: Integer, # Integer
361
+ active: :boolean, # true or false
362
+ category: ['electronics', 'home'], # Enum / List of allowed values
363
+ photo: :file, # Uploaded FilePayload
364
+ website: :url, # Valid URL (http/https)
365
+ contact: :email, # Valid email address
366
+ discount: ->(v) { v.to_f.between?(0, 100) } # Custom Lambda validation
367
+ } do |params|
173
368
  status 201
174
- { message: "User created", user: params }
369
+ { status: "ok", item: params[:title] }
175
370
  end
176
371
  ```
177
372
 
178
- ```bash
179
- curl -X POST http://localhost:4567/api/v1/users \
180
- -H "Authorization: Bearer secret" \
181
- -H "Content-Type: application/json" \
182
- -d '{"name": "Gabo"}'
373
+ ### Supported Validation Rules:
374
+
375
+ | Rule | Expected Format / Type | Valid Example |
376
+ |---|---|---|
377
+ | `:email` | Standard email format | `"contact@domain.com"` |
378
+ | `:url` | URL with `http://` or `https://` | `"https://api.domain.com"` |
379
+ | `:boolean` | Native boolean (`true` or `false`) | `true`, `false` |
380
+ | `:file` | Instance of `GRApiManager::FilePayload` | Multipart file upload |
381
+ | `Integer` | Integer number | `42`, `100`, `-10` |
382
+ | `Float` | Floating point number | `19.99`, `0.5`, `-3.14` |
383
+ | `Numeric` | Any numeric value (`Integer` or `Float`) | `10`, `3.14` |
384
+ | `String` | Non-empty string | `"Sample Text"` |
385
+ | `Array` | Array of elements | `[1, 2, 3]` |
386
+ | `Hash` | JSON Object or Hash | `{ key: "value" }` |
387
+ | `['a', 'b']` | Exact inclusion in list (Enum) | `'electronics'` |
388
+ | `/^regex$/` | Regular expression match | `"ABC-1234"` |
389
+ | `->(val) { ... }` | Custom Lambda/Proc (must return `true`) | `->(n) { n.to_i > 0 }` |
390
+
391
+ ---
392
+
393
+ ## Comprehensive File, Binary & Image Handling
183
394
 
184
- # => 400 { "error": "Missing required parameters", "required": ["email"] }
395
+ `gr_api_manager` detects incoming `Content-Type` headers and provides uniform file handling via `FilePayload`:
396
+
397
+ ### 1. Multipart Form Upload (`multipart/form-data`)
398
+ ```ruby
399
+ api.post('/profile/avatar', requires: [:user_id]) do |params|
400
+ avatar = params[:_files][:avatar] # FilePayload instance
401
+
402
+ # Save to disk (creates directories automatically if missing)
403
+ saved_path = avatar.save_to("./storage/avatars/user_#{params[:user_id]}#{avatar.extension}")
404
+
405
+ {
406
+ message: "Avatar saved",
407
+ filename: avatar.filename,
408
+ size: avatar.size,
409
+ extension: avatar.extension,
410
+ saved_path: saved_path
411
+ }
412
+ end
413
+ ```
414
+
415
+ #### cURL multipart:
416
+ ```bash
417
+ curl -X POST http://localhost:4000/profile/avatar \
418
+ -H "Authorization: Bearer my_token" \
419
+ -F "user_id=10" \
420
+ -F "avatar=@/path/to/my_photo.jpg"
185
421
  ```
186
422
 
187
423
  ---
188
424
 
189
- ## Smart Type Casting
425
+ ### 2. Raw Binary Upload (Image / PDF / Octet-Stream)
190
426
 
191
- Query string parameters are automatically cast to native Ruby types before reaching your handler.
427
+ Direct raw byte streaming in request body (no multipart form):
192
428
 
193
- | Input string | Ruby type | Value |
194
- |--------------|-----------|----------|
195
- | `"42"` | Integer | `42` |
196
- | `"3.14"` | Float | `3.14` |
197
- | `"true"` | TrueClass | `true` |
198
- | `"false"` | FalseClass| `false` |
199
- | `"hello"` | String | `"hello"`|
429
+ ```ruby
430
+ api.post('/documents/raw') do |params|
431
+ file = params[:_raw_binary] # FilePayload instance
432
+
433
+ file.save_to("./storage/docs/#{file.filename}")
434
+
435
+ {
436
+ format: "raw binary",
437
+ detected_filename: file.filename,
438
+ size_bytes: file.size,
439
+ mime_type: file.content_type,
440
+ initial_hex: file.to_hex[0..30]
441
+ }
442
+ end
443
+ ```
200
444
 
445
+ #### cURL raw binary:
201
446
  ```bash
202
- curl "http://localhost:4567/api/v1/test/types?age=18&active=true&score=9.5" \
203
- -H "Authorization: Bearer secret"
204
-
205
- # => { "age": 18, "active": true, "score": 9.5 }
447
+ curl -X POST http://localhost:4000/documents/raw \
448
+ -H "Authorization: Bearer my_token" \
449
+ -H "Content-Type: application/pdf" \
450
+ -H "Content-Disposition: attachment; filename=\"contract.pdf\"" \
451
+ --data-binary @contract.pdf
206
452
  ```
207
453
 
208
454
  ---
209
455
 
210
- ## Error Handling
456
+ ### 3. Base64 and Hexadecimal Conversion
457
+ ```ruby
458
+ api.post('/files/convert') do |params|
459
+ file = params[:_files][:file]
211
460
 
212
- Global handlers return consistent JSON for unmatched routes and unhandled exceptions.
461
+ {
462
+ base64: file.to_base64, # Clean Base64 string without newlines
463
+ hexadecimal: file.to_hex, # Lowercase hex string
464
+ total_bytes: file.size
465
+ }
466
+ end
467
+ ```
213
468
 
214
- ```json
215
- // 404
216
- { "error": "Endpoint not found", "path": "/api/v1/missing" }
469
+ ---
217
470
 
218
- // 500
219
- { "error": "Internal Server Error", "details": "..." }
471
+ ### 4. Plain Text Upload (`text/plain`)
472
+ ```ruby
473
+ api.post('/logs/text') do |params|
474
+ raw_text = params[:_raw_text] # UTF-8 String
475
+ { lines: raw_text.lines.count, characters: raw_text.length }
476
+ end
220
477
  ```
221
478
 
222
- You can also set status codes and short-circuit responses manually inside any handler:
479
+ ---
480
+
481
+ ## Serving & Downloading Files to Clients
482
+
483
+ When a route block returns a `String`, `gr_api_manager` serves it **directly as raw data**, allowing seamless downloads of images, PDFs, or binary streams:
223
484
 
224
485
  ```ruby
225
- api.get('/users/:id') do |params|
226
- if params[:id] == 0
486
+ api.get('/downloads/photo/:id', auth: false) do |params|
487
+ photo_path = "./storage/avatars/user_#{params[:id]}.jpg"
488
+
489
+ unless File.exist?(photo_path)
227
490
  status 404
228
- next { error: "Invalid user ID" }
491
+ next { error: "Photo not found" }
229
492
  end
230
493
 
231
- { id: params[:id], name: "User_#{params[:id]}" }
494
+ # Configure response headers
495
+ content_type 'image/jpeg'
496
+ headers 'Content-Disposition' => "inline; filename=\"photo_#{params[:id]}.jpg\""
497
+
498
+ # Return binary bytes directly
499
+ File.binread(photo_path)
232
500
  end
233
501
  ```
234
502
 
235
503
  ---
236
504
 
237
- ## Request Logging
505
+ ## Rate Limiting & Real IP Detection
238
506
 
239
- Every request is logged to stdout with a timestamp and color-coded status:
507
+ Protect your API with thread-safe sliding-window rate limiting per client IP:
240
508
 
241
- ```text
242
- [14:32:01] GET /api/v1/health - 200
243
- [14:32:05] POST /api/v1/users - 400
509
+ ```ruby
510
+ api = GRApiManager::Server.new(
511
+ rate_limit: 60, # Max 60 requests
512
+ rate_limit_window: 60, # per 60-second window
513
+ trust_proxy_headers: true # Reads CF-Connecting-IP, X-Real-IP, X-Forwarded-For
514
+ )
244
515
  ```
245
516
 
246
- Green for 2xx, red for everything else.
517
+ Standard HTTP response headers:
518
+ * `X-RateLimit-Limit`: Maximum allowed requests (`60`).
519
+ * `X-RateLimit-Remaining`: Remaining requests in current window.
520
+ * `X-RateLimit-Reset`: Unix timestamp when quota resets.
521
+ * `Retry-After`: Seconds to wait if rate-limited (`429 Too Many Requests`).
247
522
 
248
523
  ---
249
524
 
250
- ## Running the Server
525
+ ## Smart Parameter Casting
251
526
 
252
- ```ruby
253
- api.run!
254
- ```
527
+ URL and Query String parameters are automatically converted to native Ruby types before entering your route block:
255
528
 
256
- ```text
257
- =============================================
258
- GR API MANAGER STARTED
259
- Port : 4567
260
- Auth : Enabled
261
- Prefix : /api/v1
262
- =============================================
529
+ ```ruby
530
+ api.get('/analytics') do |params|
531
+ # Request: /analytics?id=123&active=true&discount=15.5&balance=-500&category=tech
532
+
533
+ params[:id] # => 123 (Integer)
534
+ params[:active] # => true (TrueClass)
535
+ params[:discount] # => 15.5 (Float)
536
+ params[:balance] # => -500 (Integer)
537
+ params[:category] # => "tech" (String)
538
+
539
+ { status: "ok" }
540
+ end
263
541
  ```
264
542
 
265
543
  ---
266
544
 
267
- ## Full Usage Example
268
-
269
- Below is a complete working API covering the most common patterns.
545
+ ## Responses, HTTP Status Codes & Dev Mode
270
546
 
547
+ ### Custom status codes:
271
548
  ```ruby
272
- require 'gr_api_manager'
549
+ api.post('/resources') do
550
+ status 201 # Created
551
+ { message: "Resource created" }
552
+ end
553
+ ```
273
554
 
274
- api = GRApiManager::Server.new(
275
- port: 4567,
276
- bearer_token: "secret123",
277
- prefix: "/api/v1"
278
- )
555
+ ### Developer Mode (`dev_mode: true`):
556
+ Enable `dev_mode: true` to receive structured JSON error payloads with full stack traces during development:
279
557
 
280
- # 1. Public health check — no token required
281
- api.get('/health', auth: false) do
282
- { status: 'online', time: Time.now.strftime("%Y-%m-%d %H:%M:%S") }
283
- end
558
+ ```ruby
559
+ api = GRApiManager::Server.new(dev_mode: true)
560
+ ```
284
561
 
285
- # 2. List users — token required (default)
286
- api.get('/users') do |params|
287
- users = [
288
- { id: 1, name: "Alice", role: "admin" },
289
- { id: 2, name: "Bob", role: "viewer" }
562
+ Error response on 500:
563
+ ```json
564
+ {
565
+ "error": "Internal Server Error",
566
+ "details": "undefined local variable or method 'missing_var'",
567
+ "class": "NameError",
568
+ "backtrace": [
569
+ "/app/routes/users.rb:14:in `block in setup'",
570
+ "/lib/gr_api_manager.rb:482:in `instance_exec'"
290
571
  ]
291
- { users: users, total: users.length }
292
- end
572
+ }
573
+ ```
293
574
 
294
- # 3. Get single user by ID — :id is cast to Integer automatically
295
- api.get('/users/:id') do |params|
296
- if params[:id] == 0
297
- status 404
298
- next { error: "User not found" }
299
- end
300
- { id: params[:id], name: "User_#{params[:id]}" }
301
- end
575
+ ---
302
576
 
303
- # 4. Create user — validates required fields before reaching the block
304
- api.post('/users', requires: [:name, :role]) do |params|
305
- status 201
306
- { message: "User created", user: { name: params[:name], role: params[:role] } }
307
- end
577
+ ## Concurrency & Production Deployment
308
578
 
309
- # 5. Update user
310
- api.put('/users/:id', requires: [:name]) do |params|
311
- { message: "User #{params[:id]} updated", name: params[:name] }
312
- end
579
+ ### Running with Puma in Production:
580
+ ```ruby
581
+ # Start with 4 worker processes and 4:16 threads per worker
582
+ api.run!(workers: 4, threads: '4:16')
583
+ ```
313
584
 
314
- # 6. Delete user
315
- api.delete('/users/:id') do |params|
316
- { message: "User #{params[:id]} deleted" }
317
- end
585
+ ### Production `Dockerfile`:
586
+ ```dockerfile
587
+ FROM ruby:3.3-slim
318
588
 
319
- api.run!
320
- ```
589
+ WORKDIR /app
590
+ COPY Gemfile* ./
591
+ RUN bundle install --without development test
321
592
 
322
- ### Minimal setup using only .env
593
+ COPY . .
323
594
 
324
- ```env
325
- # .env
326
- PORT=4567
327
- API_TOKEN=secret123
595
+ EXPOSE 4000
596
+ CMD ["ruby", "app.rb"]
328
597
  ```
329
598
 
330
- ```ruby
331
- # api_server.rb
332
- require 'gr_api_manager'
599
+ ---
333
600
 
334
- api = GRApiManager::Server.new # reads everything from .env
601
+ ## Automated Testing
335
602
 
336
- api.get('/ping', auth: false) { { pong: true } }
603
+ Full test suite with **RSpec** and **Rack::Test**:
337
604
 
338
- api.run!
605
+ ```bash
606
+ rspec
607
+ # => 59 examples, 0 failures
339
608
  ```
340
609
 
341
610
  ---
342
611
 
343
612
  ## License
344
613
 
345
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
614
+ This project is licensed under the [MIT](LICENSE) License. Created by **Gabo Razo**.