gr_api_manager 0.3.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 +424 -637
- data/README_ES.md +615 -0
- data/lib/gr_api_manager.rb +449 -138
- metadata +60 -13
data/README_ES.md
ADDED
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
# GR API Manager
|
|
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.md)
|
|
11
|
+
|
|
12
|
+
**GR API Manager** es un micro-framework y wrapper de alto rendimiento de Ruby sobre Sinatra y Puma, diseñado para construir APIs REST profesionales sin código repetitivo (*zero boilerplate*).
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Requisitos del Sistema
|
|
17
|
+
|
|
18
|
+
| Dependencia / Entorno | Version requerida / soportada |
|
|
19
|
+
|---|---|
|
|
20
|
+
| **Ruby** | `>= 3.0` (Probado en 3.0, 3.1, 3.2, 3.3 y 3.4) |
|
|
21
|
+
| **Sinatra** | `>= 3.0, < 5.0` |
|
|
22
|
+
| **Puma** | `>= 5.0, < 9.0` |
|
|
23
|
+
| **Dotenv** | `>= 2.8, < 4.0` |
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Caracteristicas Principales
|
|
28
|
+
|
|
29
|
+
- **Disponible en RubyGems (`0.4.0`)** - instalacion global o via Bundler.
|
|
30
|
+
- **Grupos de Rutas Modulares (`api.group`)** - organiza proyectos grandes en multiples archivos y modulos independientes con herencia de prefijos y opciones.
|
|
31
|
+
- **Autenticacion Dual (Bearer Token Fijo & JWT Nativo)** - soporte para tokens estaticos pre-compartidos y motor JWT (HMAC-SHA256) integrado sin gemas externas.
|
|
32
|
+
- **Validacion Declarativa de Esquemas y Tipos** - valida contratos de datos complejos con `:email`, `:url`, `:boolean`, `:file`, clases (`Integer`, `Float`, `String`, `Array`, `Hash`), listas enum, expresiones regulares o lambdas.
|
|
33
|
+
- **Control de Tasa (Rate Limiting 429)** - algoritmo *sliding window* con deteccion automatica de IP real tras Cloudflare (`CF-Connecting-IP`), Nginx (`X-Real-IP`) o Proxies (`X-Forwarded-For`), con soporte para almacenes en memoria o externos (Redis).
|
|
34
|
+
- **Cast Inteligente de Tipos** - conversion automatica de parametros de query/URL (`"100"` -> `100`, `"-42"` -> `-42`, `"true"` -> `true`, `"19.99"` -> `19.99`).
|
|
35
|
+
- **Manejo Integral de Archivos y Binarios (`FilePayload`)** - soporte transparente para uploads multipart, flujos binarios crudos, conversion a Base64, Hexadecimal, descarga de archivos y guardado automatico en disco.
|
|
36
|
+
- **Servidor Concurrente Puma** - soporte para multiples procesos worker y pools de threads.
|
|
37
|
+
- **Modo de Depuracion (`dev_mode`)** - errores 500 estructurados en JSON con stack trace detallado para desarrollo.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Instalacion
|
|
42
|
+
|
|
43
|
+
Agrega la gema a tu `Gemfile`:
|
|
44
|
+
```ruby
|
|
45
|
+
gem 'gr_api_manager', '~> 0.4.0'
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Y ejecuta:
|
|
49
|
+
```bash
|
|
50
|
+
bundle install
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
O instalala directamente en tu sistema:
|
|
54
|
+
```bash
|
|
55
|
+
gem install gr_api_manager
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Tabla de Contenidos
|
|
61
|
+
|
|
62
|
+
1. [Inicio Rapido](#inicio-rapido)
|
|
63
|
+
2. [Autenticacion: Token Fijo (Bearer) y JWT Nativo](#autenticacion-token-fijo-bearer-y-jwt-nativo)
|
|
64
|
+
3. [Estructura Modular Multi-Archivo (Importar y Agrupar)](#estructura-modular-multi-archivo)
|
|
65
|
+
4. [CRUD Completo con Verbos HTTP](#crud-completo-con-verbos-http)
|
|
66
|
+
5. [Validacion Declarativa de Esquemas y Tipos](#validacion-declarativa-de-esquemas-y-tipos)
|
|
67
|
+
6. [Manejo Integral de Archivos, Binarios e Imagenes](#manejo-integral-de-archivos-binarios-e-imagenes)
|
|
68
|
+
7. [Descarga y Servir Archivos al Cliente](#descarga-y-servir-archivos-al-cliente)
|
|
69
|
+
8. [Rate Limiting y Deteccion de IP Real (Cloudflare/Nginx)](#rate-limiting-y-deteccion-de-ip-real)
|
|
70
|
+
9. [Cast Inteligente de Parametros](#cast-inteligente-de-parametros)
|
|
71
|
+
10. [Respuestas, Codigos HTTP y Modo Desarrollo](#respuestas-codigos-http-y-modo-desarrollo)
|
|
72
|
+
11. [Concurrencia y Produccion (Puma & Docker)](#concurrencia-y-produccion)
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Inicio Rapido
|
|
77
|
+
|
|
78
|
+
Crea un archivo `app.rb`:
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
require 'gr_api_manager'
|
|
82
|
+
|
|
83
|
+
# Inicializa el servidor con un token fijo y clave JWT
|
|
84
|
+
api = GRApiManager::Server.new(
|
|
85
|
+
port: 4000,
|
|
86
|
+
bearer_token: "mi_token_fijo_secreto",
|
|
87
|
+
jwt_secret: "mi_firma_jwt"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Endpoint publico (sin autenticacion)
|
|
91
|
+
api.get('/health', auth: false) do
|
|
92
|
+
{ status: 'online', timestamp: Time.now.to_i }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Endpoint protegido con token fijo (default: auth = true)
|
|
96
|
+
api.get('/datos-protegidos') do
|
|
97
|
+
{ mensaje: "Acceso autorizado con Bearer Token", datos: [10, 20, 30] }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Inicia el servidor
|
|
101
|
+
api.run!
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Ejecuta tu API:
|
|
105
|
+
```bash
|
|
106
|
+
ruby app.rb
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Autenticacion: Token Fijo (Bearer) y JWT Nativo
|
|
112
|
+
|
|
113
|
+
`gr_api_manager` soporta dos esquemas de autenticacion complementarios:
|
|
114
|
+
|
|
115
|
+
### 1. Token Fijo (Static Bearer Token)
|
|
116
|
+
|
|
117
|
+
Ideal para APIs privadas, comunicacion entre microservicios, webhooks o scripts backend donde existe una clave fija pre-compartida.
|
|
118
|
+
|
|
119
|
+
#### Configuracion en `app.rb`:
|
|
120
|
+
```ruby
|
|
121
|
+
api = GRApiManager::Server.new(
|
|
122
|
+
bearer_token: "clave_secreta_empresa_2026"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Ruta publica
|
|
126
|
+
api.get('/publico', auth: false) do
|
|
127
|
+
{ estado: "acceso libre" }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Rutas protegidas (por defecto auth: true)
|
|
131
|
+
api.get('/admin/config') do
|
|
132
|
+
{ base_datos: "conectada", entorno: "produccion" }
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
api.post('/admin/reiniciar', requires: [:motivo]) do |params|
|
|
136
|
+
{ accion: "reiniciando", motivo: params[:motivo] }
|
|
137
|
+
end
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
#### Como consumir desde cURL o clientes HTTP:
|
|
141
|
+
|
|
142
|
+
**Peticion valida (200 OK):**
|
|
143
|
+
```bash
|
|
144
|
+
curl -H "Authorization: Bearer clave_secreta_empresa_2026" http://localhost:4000/admin/config
|
|
145
|
+
# => {"base_datos":"conectada","entorno":"produccion"}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Peticion sin cabecera (401 Unauthorized):**
|
|
149
|
+
```bash
|
|
150
|
+
curl http://localhost:4000/admin/config
|
|
151
|
+
# => 401 {"error":"Token required. Format: 'Bearer <token>'"}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Peticion con token invalido (403 Forbidden):**
|
|
155
|
+
```bash
|
|
156
|
+
curl -H "Authorization: Bearer token_falso" http://localhost:4000/admin/config
|
|
157
|
+
# => 403 {"error":"Invalid token"}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
### 2. Autenticacion JWT Nativa (HS256)
|
|
163
|
+
|
|
164
|
+
Ideal para APIs con usuarios finales donde se emiten tokens dinamicos con roles y tiempo de expiracion.
|
|
165
|
+
|
|
166
|
+
#### Configuracion y Flujo Completo:
|
|
167
|
+
```ruby
|
|
168
|
+
api = GRApiManager::Server.new(
|
|
169
|
+
jwt_secret: "clave_secreta_para_firmar_jwts"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# 1. Login: genera y entrega el token al usuario
|
|
173
|
+
api.post('/auth/login', auth: false, requires: { email: :email, password: String }) do |params|
|
|
174
|
+
# Validar credenciales contra base de datos
|
|
175
|
+
if params[:email] == "admin@empresa.com" && params[:password] == "pass123"
|
|
176
|
+
token = api.jwt_encode(
|
|
177
|
+
{ user_id: 42, email: params[:email], role: "admin" },
|
|
178
|
+
exp: Time.now.to_i + 3600 # Expira en 1 hora
|
|
179
|
+
)
|
|
180
|
+
{ token: token, token_type: "Bearer", expira_en: 3600 }
|
|
181
|
+
else
|
|
182
|
+
status 401
|
|
183
|
+
{ error: "Credenciales invalidas" }
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# 2. Ruta protegida por JWT: inyecta automaticamente params[:current_user]
|
|
188
|
+
api.get('/perfil', auth: :jwt) do |params|
|
|
189
|
+
usuario = params[:current_user]
|
|
190
|
+
{
|
|
191
|
+
mensaje: "Token JWT valido",
|
|
192
|
+
id_usuario: usuario[:user_id],
|
|
193
|
+
email: usuario[:email],
|
|
194
|
+
rol: usuario[:role]
|
|
195
|
+
}
|
|
196
|
+
end
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Estructura Modular Multi-Archivo
|
|
202
|
+
|
|
203
|
+
Divide tu API en modulos independientes y ordenados dentro de una carpeta `routes/`:
|
|
204
|
+
|
|
205
|
+
```text
|
|
206
|
+
mi_proyecto/
|
|
207
|
+
├── app.rb # Archivo principal de configuracion y arranque
|
|
208
|
+
├── .env # Variables de entorno
|
|
209
|
+
├── Gemfile
|
|
210
|
+
└── routes/
|
|
211
|
+
├── auth_routes.rb # Login y registro
|
|
212
|
+
├── admin_routes.rb # Panel de administracion
|
|
213
|
+
├── pagos_routes.rb # Pasarela de pagos
|
|
214
|
+
└── archivos_routes.rb # Subida y descarga de archivos
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Modulo 1: `routes/auth_routes.rb`
|
|
218
|
+
```ruby
|
|
219
|
+
module AuthRoutes
|
|
220
|
+
def self.setup(router, api_server)
|
|
221
|
+
router.post('/login', auth: false, requires: { email: :email, password: String }) do |params|
|
|
222
|
+
if params[:email] == "admin@empresa.com" && params[:password] == "secreto"
|
|
223
|
+
token = api_server.jwt_encode({ user_id: 1, email: params[:email], role: "admin" })
|
|
224
|
+
{ token: token }
|
|
225
|
+
else
|
|
226
|
+
status 401
|
|
227
|
+
{ error: "Credenciales incorrectas" }
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Modulo 2: `routes/admin_routes.rb`
|
|
235
|
+
```ruby
|
|
236
|
+
module AdminRoutes
|
|
237
|
+
def self.setup(router)
|
|
238
|
+
router.get('/metricas') do |params|
|
|
239
|
+
{ cpu: "12%", memoria: "380MB", usuario: params[:current_user][:email] }
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
router.delete('/usuarios/:id') do |params|
|
|
243
|
+
{ mensaje: "Usuario #{params[:id]} eliminado" }
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Modulo 3: `routes/archivos_routes.rb`
|
|
250
|
+
```ruby
|
|
251
|
+
module ArchivosRoutes
|
|
252
|
+
def self.setup(router)
|
|
253
|
+
router.post('/subir', requires: [:nombre]) do |params|
|
|
254
|
+
archivo = params[:_files][:documento]
|
|
255
|
+
ruta = archivo.save_to("./almacen/#{params[:nombre]}#{archivo.extension}")
|
|
256
|
+
{ status: "guardado", ruta: ruta, tamano: archivo.size }
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Archivo Principal: `app.rb`
|
|
263
|
+
```ruby
|
|
264
|
+
require 'gr_api_manager'
|
|
265
|
+
|
|
266
|
+
# Importar los modulos de rutas
|
|
267
|
+
require_relative 'routes/auth_routes'
|
|
268
|
+
require_relative 'routes/admin_routes'
|
|
269
|
+
require_relative 'routes/archivos_routes'
|
|
270
|
+
|
|
271
|
+
api = GRApiManager::Server.new(
|
|
272
|
+
port: 4000,
|
|
273
|
+
jwt_secret: ENV['JWT_SECRET'] || "clave_jwt_por_defecto",
|
|
274
|
+
bearer_token: ENV['API_TOKEN'] || "token_fijo_global"
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
# Ruta raiz
|
|
278
|
+
api.get('/', auth: false) { { servicio: "API Central v1.0" } }
|
|
279
|
+
|
|
280
|
+
# Montar los grupos de rutas
|
|
281
|
+
api.group('/auth') { |g| AuthRoutes.setup(g, api) }
|
|
282
|
+
api.group('/admin', auth: :jwt) { |g| AdminRoutes.setup(g) }
|
|
283
|
+
api.group('/archivos', auth: true) { |g| ArchivosRoutes.setup(g) }
|
|
284
|
+
|
|
285
|
+
api.run!(workers: 2, threads: '2:8')
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## CRUD Completo con Verbos HTTP
|
|
291
|
+
|
|
292
|
+
Ejemplo de gestion completa de un recurso `/productos`:
|
|
293
|
+
|
|
294
|
+
```ruby
|
|
295
|
+
api = GRApiManager::Server.new(prefix: '/api/v1')
|
|
296
|
+
|
|
297
|
+
# 1. LISTAR (GET) - con parametros query auto-casteados
|
|
298
|
+
api.get('/productos', auth: false) do |params|
|
|
299
|
+
pagina = params[:pagina] || 1 # Integer
|
|
300
|
+
limite = params[:limite] || 10 # Integer
|
|
301
|
+
activo = params[:activo] != false # Boolean
|
|
302
|
+
|
|
303
|
+
{
|
|
304
|
+
pagina: pagina,
|
|
305
|
+
limite: limite,
|
|
306
|
+
items: [
|
|
307
|
+
{ id: 1, nombre: "Teclado Mecanico", precio: 89.99, activo: true },
|
|
308
|
+
{ id: 2, nombre: "Monitor 4K", precio: 299.99, activo: true }
|
|
309
|
+
]
|
|
310
|
+
}
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# 2. OBTENER POR ID (GET)
|
|
314
|
+
api.get('/productos/:id', auth: false) do |params|
|
|
315
|
+
id = params[:id] # Integer automatico
|
|
316
|
+
{ id: id, nombre: "Producto #{id}", precio: 49.99 }
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# 3. CREAR (POST) - con validacion de tipos
|
|
320
|
+
api.post('/productos', requires: { nombre: String, precio: Float, categoria: ['tech', 'oficina'] }) do |params|
|
|
321
|
+
status 201
|
|
322
|
+
{
|
|
323
|
+
mensaje: "Producto creado",
|
|
324
|
+
producto: { id: rand(100..999), nombre: params[:nombre], precio: params[:precio] }
|
|
325
|
+
}
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# 4. REEMPLAZAR COMPLETO (PUT)
|
|
329
|
+
api.put('/productos/:id', requires: { nombre: String, precio: Float }) do |params|
|
|
330
|
+
{
|
|
331
|
+
mensaje: "Producto #{params[:id]} actualizado por completo",
|
|
332
|
+
datos: params
|
|
333
|
+
}
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# 5. ACTUALIZACION PARCIAL (PATCH)
|
|
337
|
+
api.patch('/productos/:id') do |params|
|
|
338
|
+
{
|
|
339
|
+
mensaje: "Campos modificados en producto #{params[:id]}",
|
|
340
|
+
cambios: params.except(:id)
|
|
341
|
+
}
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# 6. ELIMINAR (DELETE)
|
|
345
|
+
api.delete('/productos/:id') do |params|
|
|
346
|
+
{ mensaje: "Producto #{params[:id]} eliminado con exito" }
|
|
347
|
+
end
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## Validacion Declarativa de Esquemas y Tipos
|
|
353
|
+
|
|
354
|
+
La opcion `requires:` permite validar tipos de datos, formatos de texto, archivos y reglas personalizadas:
|
|
355
|
+
|
|
356
|
+
```ruby
|
|
357
|
+
api.post '/catalogo', requires: {
|
|
358
|
+
codigo: /^[A-Z]{3}-\d{4}$/, # Regex: ej. "PRO-1234"
|
|
359
|
+
titulo: String, # Cadena no vacia
|
|
360
|
+
precio: Float, # Numero decimal
|
|
361
|
+
stock: Integer, # Numero entero
|
|
362
|
+
activo: :boolean, # true o false
|
|
363
|
+
categoria: ['electronica', 'hogar'], # Enum / Lista de opciones
|
|
364
|
+
foto: :file, # Archivo subido (FilePayload)
|
|
365
|
+
web_fab: :url, # URL valida (http/https)
|
|
366
|
+
contacto: :email, # Correo electronico valido
|
|
367
|
+
descuento: ->(v) { v.to_f.between?(0, 100) } # Lambda personalizada
|
|
368
|
+
} do |params|
|
|
369
|
+
status 201
|
|
370
|
+
{ status: "ok", item: params[:titulo] }
|
|
371
|
+
end
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### Tabla de Reglas de Validacion:
|
|
375
|
+
|
|
376
|
+
| Regla | Tipo / Formato | Ejemplo valido |
|
|
377
|
+
|---|---|---|
|
|
378
|
+
| `:email` | Correo electronico estandar | `"contacto@dominio.com"` |
|
|
379
|
+
| `:url` | URL con protocolo `http://` o `https://` | `"https://api.empresa.com"` |
|
|
380
|
+
| `:boolean` | Booleano nativo (`true` o `false`) | `true`, `false` |
|
|
381
|
+
| `:file` | Instancia de `GRApiManager::FilePayload` | Archivo subido via multipart |
|
|
382
|
+
| `Integer` | Numero entero | `42`, `100`, `-10` |
|
|
383
|
+
| `Float` | Numero de coma flotante | `19.99`, `0.5`, `-3.14` |
|
|
384
|
+
| `Numeric` | Cualquier numero (`Integer` o `Float`) | `10`, `3.14` |
|
|
385
|
+
| `String` | Cadena de texto no vacia | `"Texto"` |
|
|
386
|
+
| `Array` | Arreglo de elementos | `[1, 2, 3]` |
|
|
387
|
+
| `Hash` | Objeto o diccionario JSON | `{ clave: "valor" }` |
|
|
388
|
+
| `['a', 'b']` | Inclusion obligatoria en lista (Enum) | `'electronica'` |
|
|
389
|
+
| `/^regex$/` | Expresion regular | `"ABC-1234"` |
|
|
390
|
+
| `->(val) { ... }` | Funcion / Lambda (debe retornar `true`) | `->(n) { n.to_i > 0 }` |
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
## Manejo Integral de Archivos, Binarios e Imagenes
|
|
395
|
+
|
|
396
|
+
`gr_api_manager` detecta automaticamente el `Content-Type` de la peticion y unifica el acceso mediante la clase `FilePayload`:
|
|
397
|
+
|
|
398
|
+
### 1. Subida Multipart (`multipart/form-data`)
|
|
399
|
+
```ruby
|
|
400
|
+
api.post('/perfil/avatar', requires: [:usuario_id]) do |params|
|
|
401
|
+
avatar = params[:_files][:avatar] # FilePayload
|
|
402
|
+
|
|
403
|
+
# Guardar en disco (crea carpetas intermedias automaticamente)
|
|
404
|
+
ruta = avatar.save_to("./almacen/avatares/user_#{params[:usuario_id]}#{avatar.extension}")
|
|
405
|
+
|
|
406
|
+
{
|
|
407
|
+
mensaje: "Avatar guardado",
|
|
408
|
+
archivo: avatar.filename,
|
|
409
|
+
tamano: avatar.size,
|
|
410
|
+
extension: avatar.extension,
|
|
411
|
+
guardado_en: ruta
|
|
412
|
+
}
|
|
413
|
+
end
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
#### cURL multipart:
|
|
417
|
+
```bash
|
|
418
|
+
curl -X POST http://localhost:4000/perfil/avatar \
|
|
419
|
+
-H "Authorization: Bearer mi_token" \
|
|
420
|
+
-F "usuario_id=10" \
|
|
421
|
+
-F "avatar=@/ruta/a/mi_foto.jpg"
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
---
|
|
425
|
+
|
|
426
|
+
### 2. Subida de Binario Crudo (Raw Binary / Image / PDF Stream)
|
|
427
|
+
|
|
428
|
+
Envio directo de bytes en el body de la peticion (sin multipart):
|
|
429
|
+
|
|
430
|
+
```ruby
|
|
431
|
+
api.post('/documentos/raw') do |params|
|
|
432
|
+
archivo = params[:_raw_binary] # FilePayload
|
|
433
|
+
|
|
434
|
+
archivo.save_to("./almacen/docs/#{archivo.filename}")
|
|
435
|
+
|
|
436
|
+
{
|
|
437
|
+
formato: "binario crudo",
|
|
438
|
+
nombre_detectado: archivo.filename,
|
|
439
|
+
tamano_bytes: archivo.size,
|
|
440
|
+
mime_type: archivo.content_type,
|
|
441
|
+
hex_inicial: archivo.to_hex[0..30]
|
|
442
|
+
}
|
|
443
|
+
end
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
#### cURL binario crudo:
|
|
447
|
+
```bash
|
|
448
|
+
curl -X POST http://localhost:4000/documentos/raw \
|
|
449
|
+
-H "Authorization: Bearer mi_token" \
|
|
450
|
+
-H "Content-Type: application/pdf" \
|
|
451
|
+
-H "Content-Disposition: attachment; filename=\"contrato.pdf\"" \
|
|
452
|
+
--data-binary @contrato.pdf
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
### 3. Conversion a Base64 y Hexadecimal
|
|
458
|
+
```ruby
|
|
459
|
+
api.post('/archivos/convertir') do |params|
|
|
460
|
+
archivo = params[:_files][:archivo]
|
|
461
|
+
|
|
462
|
+
{
|
|
463
|
+
base64: archivo.to_base64, # Cadena Base64 limpia (sin saltos de linea)
|
|
464
|
+
hexadecimal: archivo.to_hex, # Cadena Hexadecimal en minusculas
|
|
465
|
+
bytes_totales: archivo.size
|
|
466
|
+
}
|
|
467
|
+
end
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
---
|
|
471
|
+
|
|
472
|
+
### 4. Subida en Texto Plano (`text/plain`)
|
|
473
|
+
```ruby
|
|
474
|
+
api.post('/logs/texto') do |params|
|
|
475
|
+
texto_crudo = params[:_raw_text] # String UTF-8
|
|
476
|
+
{ lineas: texto_crudo.lines.count, caracteres: texto_crudo.length }
|
|
477
|
+
end
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
---
|
|
481
|
+
|
|
482
|
+
## Descarga y Servir Archivos al Cliente
|
|
483
|
+
|
|
484
|
+
Si retornas un `String` desde el bloque de la ruta, `gr_api_manager` lo entrega **directamente como flujo de datos**, permitiendo servir imagenes, PDFs o descargas binarias:
|
|
485
|
+
|
|
486
|
+
```ruby
|
|
487
|
+
api.get('/descargas/foto/:id', auth: false) do |params|
|
|
488
|
+
ruta_foto = "./almacen/avatares/user_#{params[:id]}.jpg"
|
|
489
|
+
|
|
490
|
+
unless File.exist?(ruta_foto)
|
|
491
|
+
status 404
|
|
492
|
+
next { error: "Foto no encontrada" }
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
# Configurar cabeceras de respuesta HTTP
|
|
496
|
+
content_type 'image/jpeg'
|
|
497
|
+
headers 'Content-Disposition' => "inline; filename=\"foto_#{params[:id]}.jpg\""
|
|
498
|
+
|
|
499
|
+
# Retornar los bytes del archivo directamente
|
|
500
|
+
File.binread(ruta_foto)
|
|
501
|
+
end
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
---
|
|
505
|
+
|
|
506
|
+
## Rate Limiting y Deteccion de IP Real
|
|
507
|
+
|
|
508
|
+
Protege tu API con control de tasa deslizante (*sliding-window*) por IP de cliente:
|
|
509
|
+
|
|
510
|
+
```ruby
|
|
511
|
+
api = GRApiManager::Server.new(
|
|
512
|
+
rate_limit: 60, # Maximo 60 peticiones
|
|
513
|
+
rate_limit_window: 60, # por cada ventana de 60 segundos
|
|
514
|
+
trust_proxy_headers: true # Lee CF-Connecting-IP, X-Real-IP, X-Forwarded-For
|
|
515
|
+
)
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
Cabeceras HTTP devueltas en cada peticion:
|
|
519
|
+
* `X-RateLimit-Limit`: Limite maximo permitido (`60`).
|
|
520
|
+
* `X-RateLimit-Remaining`: Peticiones restantes en la ventana actual.
|
|
521
|
+
* `X-RateLimit-Reset`: Timestamp Unix cuando se reinicia la cuota.
|
|
522
|
+
* `Retry-After`: Segundos a esperar si se excede el limite (`429 Too Many Requests`).
|
|
523
|
+
|
|
524
|
+
---
|
|
525
|
+
|
|
526
|
+
## Cast Inteligente de Parametros
|
|
527
|
+
|
|
528
|
+
Los parametros de URL y Query String se transforman automaticamente a tipos nativos de Ruby:
|
|
529
|
+
|
|
530
|
+
```ruby
|
|
531
|
+
api.get('/analisis') do |params|
|
|
532
|
+
# Peticion: /analisis?id=123&activo=true&descuento=15.5&saldo=-500&categoria=tech
|
|
533
|
+
|
|
534
|
+
params[:id] # => 123 (Integer)
|
|
535
|
+
params[:activo] # => true (TrueClass)
|
|
536
|
+
params[:descuento] # => 15.5 (Float)
|
|
537
|
+
params[:saldo] # => -500 (Integer)
|
|
538
|
+
params[:categoria] # => "tech" (String)
|
|
539
|
+
|
|
540
|
+
{ status: "ok" }
|
|
541
|
+
end
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
---
|
|
545
|
+
|
|
546
|
+
## Respuestas, Codigos HTTP y Modo Desarrollo
|
|
547
|
+
|
|
548
|
+
### Codigos de estado personalizados:
|
|
549
|
+
```ruby
|
|
550
|
+
api.post('/recursos') do
|
|
551
|
+
status 201 # Created
|
|
552
|
+
{ mensaje: "Recurso creado" }
|
|
553
|
+
end
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
### Modo Desarrollo (`dev_mode: true`):
|
|
557
|
+
En desarrollo, activa `dev_mode: true` para obtener detalles exactos y stack traces en JSON al ocurrir un error inesperado (500):
|
|
558
|
+
|
|
559
|
+
```ruby
|
|
560
|
+
api = GRApiManager::Server.new(dev_mode: true)
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
Respuesta en 500:
|
|
564
|
+
```json
|
|
565
|
+
{
|
|
566
|
+
"error": "Internal Server Error",
|
|
567
|
+
"details": "undefined local variable or method 'variable_inexistente'",
|
|
568
|
+
"class": "NameError",
|
|
569
|
+
"backtrace": [
|
|
570
|
+
"/app/routes/usuarios.rb:14:in `block in setup'",
|
|
571
|
+
"/lib/gr_api_manager.rb:482:in `instance_exec'"
|
|
572
|
+
]
|
|
573
|
+
}
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
---
|
|
577
|
+
|
|
578
|
+
## Concurrencia y Produccion
|
|
579
|
+
|
|
580
|
+
### Ejecutar con Puma en Produccion:
|
|
581
|
+
```ruby
|
|
582
|
+
# Inicia con 4 procesos worker y entre 4 y 16 threads por worker
|
|
583
|
+
api.run!(workers: 4, threads: '4:16')
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
### Dockerfile de Produccion:
|
|
587
|
+
```dockerfile
|
|
588
|
+
FROM ruby:3.3-slim
|
|
589
|
+
|
|
590
|
+
WORKDIR /app
|
|
591
|
+
COPY Gemfile* ./
|
|
592
|
+
RUN bundle install --without development test
|
|
593
|
+
|
|
594
|
+
COPY . .
|
|
595
|
+
|
|
596
|
+
EXPOSE 4000
|
|
597
|
+
CMD ["ruby", "app.rb"]
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
---
|
|
601
|
+
|
|
602
|
+
## Pruebas Automatizadas
|
|
603
|
+
|
|
604
|
+
El framework incluye una suite completa con **RSpec** y **Rack::Test**:
|
|
605
|
+
|
|
606
|
+
```bash
|
|
607
|
+
rspec
|
|
608
|
+
# => 59 examples, 0 failures
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
---
|
|
612
|
+
|
|
613
|
+
## Licencia
|
|
614
|
+
|
|
615
|
+
Este proyecto esta bajo la licencia [MIT](LICENSE). Creado por **Gabo Razo**.
|