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.
Files changed (5) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +424 -637
  3. data/README_ES.md +615 -0
  4. data/lib/gr_api_manager.rb +449 -138
  5. metadata +60 -13
data/README_ES.md ADDED
@@ -0,0 +1,615 @@
1
+ # GR API Manager
2
+
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 in English](https://img.shields.io/badge/README-English-blue.svg?logo=google-translate&logoColor=white)](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**.