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.
- checksums.yaml +4 -4
- data/README.md +456 -187
- data/README_ES.md +615 -0
- data/lib/gr_api_manager.rb +766 -59
- metadata +72 -8
data/README.md
CHANGED
|
@@ -1,345 +1,614 @@
|
|
|
1
1
|
# GR API Manager
|
|
2
2
|
|
|
3
|
-
[](https://rubygems.org/gems/gr_api_manager)
|
|
4
|
+
[](https://rubygems.org/gems/gr_api_manager)
|
|
5
|
+
[](https://www.ruby-lang.org/)
|
|
6
|
+
[](https://sinatrarb.com/)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
[](https://rubygems.org/gems/gr_api_manager)
|
|
9
|
+
[](spec/)
|
|
10
|
+
[](README_ES.md)
|
|
4
11
|
|
|
5
|
-
|
|
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
|
-
##
|
|
16
|
+
## System Requirements
|
|
10
17
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
24
42
|
|
|
43
|
+
Add the gem to your `Gemfile`:
|
|
25
44
|
```ruby
|
|
26
|
-
gem '
|
|
45
|
+
gem 'gr_api_manager', '~> 0.4.0'
|
|
27
46
|
```
|
|
28
47
|
|
|
29
|
-
And
|
|
48
|
+
And run:
|
|
30
49
|
```bash
|
|
31
50
|
bundle install
|
|
32
51
|
```
|
|
33
52
|
|
|
34
|
-
Or install
|
|
53
|
+
Or install directly:
|
|
35
54
|
```bash
|
|
36
|
-
gem install
|
|
55
|
+
gem install gr_api_manager
|
|
37
56
|
```
|
|
38
57
|
|
|
39
58
|
---
|
|
40
59
|
|
|
41
|
-
##
|
|
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:
|
|
48
|
-
bearer_token: "
|
|
49
|
-
|
|
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',
|
|
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
|
-
|
|
62
|
-
# => {"status":"online","time":"..."}
|
|
106
|
+
ruby app.rb
|
|
63
107
|
```
|
|
64
108
|
|
|
65
109
|
---
|
|
66
110
|
|
|
67
|
-
##
|
|
111
|
+
## Authentication: Static Bearer Token & Native JWT
|
|
112
|
+
|
|
113
|
+
`gr_api_manager` supports two complementary authentication schemes:
|
|
68
114
|
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
125
|
+
# Public route
|
|
126
|
+
api.get('/public', auth: false) do
|
|
127
|
+
{ status: "open access" }
|
|
128
|
+
end
|
|
81
129
|
|
|
82
|
-
|
|
130
|
+
# Protected routes (default auth: true)
|
|
131
|
+
api.get('/admin/config') do
|
|
132
|
+
{ database: "connected", environment: "production" }
|
|
133
|
+
end
|
|
83
134
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
135
|
+
api.post('/admin/restart', requires: [:reason]) do |params|
|
|
136
|
+
{ action: "restarting", reason: params[:reason] }
|
|
137
|
+
end
|
|
87
138
|
```
|
|
88
139
|
|
|
89
|
-
|
|
140
|
+
#### Consuming via cURL:
|
|
90
141
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
### 2. Native JWT Authentication (HS256)
|
|
106
163
|
|
|
107
|
-
|
|
164
|
+
Ideal for end-user facing APIs issuing dynamic tokens with expiration and claims.
|
|
108
165
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
new(
|
|
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
|
-
##
|
|
200
|
+
## Multi-File Modular Architecture
|
|
201
|
+
|
|
202
|
+
Divide your API into dedicated, clean route modules within a `routes/` directory:
|
|
117
203
|
|
|
118
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## Full REST CRUD with HTTP Verbs
|
|
290
|
+
|
|
291
|
+
Example managing a `/products` resource:
|
|
148
292
|
|
|
149
293
|
```ruby
|
|
150
|
-
api.
|
|
151
|
-
|
|
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
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
162
|
-
{
|
|
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
|
-
##
|
|
351
|
+
## Declarative Schema & Type Validation
|
|
168
352
|
|
|
169
|
-
|
|
353
|
+
The `requires:` option validates data types, string formats, uploaded files, and custom logic:
|
|
170
354
|
|
|
171
355
|
```ruby
|
|
172
|
-
api.post
|
|
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
|
-
{
|
|
369
|
+
{ status: "ok", item: params[:title] }
|
|
175
370
|
end
|
|
176
371
|
```
|
|
177
372
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
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
|
-
|
|
425
|
+
### 2. Raw Binary Upload (Image / PDF / Octet-Stream)
|
|
190
426
|
|
|
191
|
-
|
|
427
|
+
Direct raw byte streaming in request body (no multipart form):
|
|
192
428
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
|
203
|
-
-H "Authorization: Bearer
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
456
|
+
### 3. Base64 and Hexadecimal Conversion
|
|
457
|
+
```ruby
|
|
458
|
+
api.post('/files/convert') do |params|
|
|
459
|
+
file = params[:_files][:file]
|
|
211
460
|
|
|
212
|
-
|
|
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
|
-
|
|
215
|
-
// 404
|
|
216
|
-
{ "error": "Endpoint not found", "path": "/api/v1/missing" }
|
|
469
|
+
---
|
|
217
470
|
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
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('/
|
|
226
|
-
|
|
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: "
|
|
491
|
+
next { error: "Photo not found" }
|
|
229
492
|
end
|
|
230
493
|
|
|
231
|
-
|
|
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
|
-
##
|
|
505
|
+
## Rate Limiting & Real IP Detection
|
|
238
506
|
|
|
239
|
-
|
|
507
|
+
Protect your API with thread-safe sliding-window rate limiting per client IP:
|
|
240
508
|
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
525
|
+
## Smart Parameter Casting
|
|
251
526
|
|
|
252
|
-
|
|
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
|
-
```
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
549
|
+
api.post('/resources') do
|
|
550
|
+
status 201 # Created
|
|
551
|
+
{ message: "Resource created" }
|
|
552
|
+
end
|
|
553
|
+
```
|
|
273
554
|
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
281
|
-
api.
|
|
282
|
-
|
|
283
|
-
end
|
|
558
|
+
```ruby
|
|
559
|
+
api = GRApiManager::Server.new(dev_mode: true)
|
|
560
|
+
```
|
|
284
561
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
292
|
-
|
|
572
|
+
}
|
|
573
|
+
```
|
|
293
574
|
|
|
294
|
-
|
|
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
|
-
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
end
|
|
585
|
+
### Production `Dockerfile`:
|
|
586
|
+
```dockerfile
|
|
587
|
+
FROM ruby:3.3-slim
|
|
318
588
|
|
|
319
|
-
|
|
320
|
-
|
|
589
|
+
WORKDIR /app
|
|
590
|
+
COPY Gemfile* ./
|
|
591
|
+
RUN bundle install --without development test
|
|
321
592
|
|
|
322
|
-
|
|
593
|
+
COPY . .
|
|
323
594
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
PORT=4567
|
|
327
|
-
API_TOKEN=secret123
|
|
595
|
+
EXPOSE 4000
|
|
596
|
+
CMD ["ruby", "app.rb"]
|
|
328
597
|
```
|
|
329
598
|
|
|
330
|
-
|
|
331
|
-
# api_server.rb
|
|
332
|
-
require 'gr_api_manager'
|
|
599
|
+
---
|
|
333
600
|
|
|
334
|
-
|
|
601
|
+
## Automated Testing
|
|
335
602
|
|
|
336
|
-
|
|
603
|
+
Full test suite with **RSpec** and **Rack::Test**:
|
|
337
604
|
|
|
338
|
-
|
|
605
|
+
```bash
|
|
606
|
+
rspec
|
|
607
|
+
# => 59 examples, 0 failures
|
|
339
608
|
```
|
|
340
609
|
|
|
341
610
|
---
|
|
342
611
|
|
|
343
612
|
## License
|
|
344
613
|
|
|
345
|
-
|
|
614
|
+
This project is licensed under the [MIT](LICENSE) License. Created by **Gabo Razo**.
|