bug_bunny 3.1.6 → 4.0.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/CHANGELOG.md +17 -0
- data/README.md +48 -28
- data/lib/bug_bunny/consumer.rb +63 -114
- data/lib/bug_bunny/routing/route.rb +103 -0
- data/lib/bug_bunny/routing/route_set.rb +268 -0
- data/lib/bug_bunny/version.rb +1 -1
- data/lib/bug_bunny.rb +8 -0
- data/lib/generators/bug_bunny/install/templates/initializer.rb +17 -0
- data/test/test_helper.rb +11 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b06b89852caa37c57d4db098d7f2f51f6d6f7361898ce5fc3484ad81e31068fd
|
|
4
|
+
data.tar.gz: cc53173b422e52e54e3a3a8af54676f6a954279ab81847335be3b37a5251b882
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6643abb20472bea870426d694d3481c6ced6bd1bf2d0a26e6cbc0526f60050b016de265785611cd4e7046304cc88058104845172d676475be1dc7c6b6ad79d10
|
|
7
|
+
data.tar.gz: 3f60029f149d765752616323fcf53ce9406ca80e3cffeeaf008bf8a54daac0f06366495081629b1f22127e6bc171080a5946f89341d5cebb5929596d70275bc1
|
data/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
|
+
## [4.0.0] - 2026-03-02
|
|
3
|
+
|
|
4
|
+
### ⚠ Breaking Changes
|
|
5
|
+
* **Declarative Routing (Rails-style):** El enrutamiento "mágico" y heurístico del Consumer ha sido reemplazado por un motor de enrutamiento explícito y estricto.
|
|
6
|
+
* Ahora es **obligatorio** definir un mapa de rutas usando el DSL `BugBunny.routes.draw` (típicamente en un inicializador como `config/initializers/bug_bunny_routes.rb`).
|
|
7
|
+
* Los mensajes entrantes cuyas rutas no estén explícitamente declaradas serán rechazados inmediatamente con un error `404 Not Found`.
|
|
8
|
+
|
|
9
|
+
### 🚀 New Features & Architecture
|
|
10
|
+
* **Advanced Routing DSL:** Se construyó un motor de enrutamiento completo y robusto inspirado en `ActionDispatch::Routing` de Rails.
|
|
11
|
+
* **Smart Route Parameters:** Compilación de rutas a expresiones regulares, permitiendo la extracción nativa de parámetros dinámicos desde la URL (ej. `get 'clusters/:cluster_id/nodes/:id/metrics'`). Estos se inyectan automáticamente en el hash `params` del Controlador.
|
|
12
|
+
* **Resource Macros & Filtering:** Introducción del macro `resources :name` para generar endpoints CRUD estándar. Ahora soporta filtrado granular de acciones utilizando las opciones `only:` y `except:`.
|
|
13
|
+
* **Nested Scopes (Member/Collection):** Soporte total para bloques anidados `member do ... end` y `collection do ... end` dentro de los recursos, permitiendo definir rutas complejas infiriendo automáticamente el controlador destino y la inyección del `:id`.
|
|
14
|
+
|
|
15
|
+
### 🛡️ Security & Observability
|
|
16
|
+
* **Strict Instantiation (RCE Prevention):** Al requerir que todas las rutas sean declaradas explícitamente por el desarrollador, se elimina por completo el vector de ataque que permitía intentar instanciar clases arbitrarias de Ruby manipulando el header `type`.
|
|
17
|
+
* **Enhanced Routing Logs:** El Consumer ahora emite un log de nivel `DEBUG` (marcado con 🎯) que confirma de manera transparente exactamente qué Controlador y Acción se resolvieron al evaluar la petición contra el mapa de rutas.
|
|
18
|
+
|
|
2
19
|
## [3.1.6] - 2026-02-27
|
|
3
20
|
|
|
4
21
|
### 🐛 Bug Fixes & Router Improvements
|
data/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
**Active Record over AMQP.**
|
|
6
6
|
|
|
7
|
-
BugBunny transforma la complejidad de la mensajería asíncrona (RabbitMQ) en una arquitectura **RESTful familiar** para desarrolladores Rails. Envía mensajes como si estuvieras usando Active Record y procésalos como si fueran Controladores de Rails.
|
|
7
|
+
BugBunny transforma la complejidad de la mensajería asíncrona (RabbitMQ) en una arquitectura **RESTful familiar** para desarrolladores Rails. Envía mensajes como si estuvieras usando Active Record y procésalos como si fueran Controladores de Rails, apoyado por un potente motor de enrutamiento declarativo.
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
@@ -12,14 +12,14 @@ BugBunny transforma la complejidad de la mensajería asíncrona (RabbitMQ) en un
|
|
|
12
12
|
- [Introducción: La Filosofía](#-introducción-la-filosofía)
|
|
13
13
|
- [Instalación](#-instalación)
|
|
14
14
|
- [Configuración Inicial](#-configuración-inicial)
|
|
15
|
-
- [Configuración de Infraestructura en Cascada](#-configuración-de-infraestructura-en-cascada
|
|
15
|
+
- [Configuración de Infraestructura en Cascada](#-configuración-de-infraestructura-en-cascada)
|
|
16
16
|
- [Modo Cliente: Recursos (ORM)](#-modo-cliente-recursos-orm)
|
|
17
17
|
- [Definición y Atributos](#1-definición-y-atributos-híbridos)
|
|
18
18
|
- [CRUD y Consultas](#2-crud-y-consultas-restful)
|
|
19
19
|
- [Contexto Dinámico (.with)](#3-contexto-dinámico-with)
|
|
20
20
|
- [Client Middleware](#4-client-middleware-interceptores)
|
|
21
21
|
- [Modo Servidor: Controladores](#-modo-servidor-controladores)
|
|
22
|
-
- [Ruteo
|
|
22
|
+
- [Ruteo Declarativo (Rutas)](#1-ruteo-declarativo-rutas)
|
|
23
23
|
- [El Controlador](#2-el-controlador)
|
|
24
24
|
- [Manejo de Errores](#3-manejo-de-errores-declarativo)
|
|
25
25
|
- [Observabilidad y Tracing](#-observabilidad-y-tracing)
|
|
@@ -33,7 +33,7 @@ En lugar de pensar en "Exchanges" y "Queues", BugBunny inyecta verbos HTTP (`GET
|
|
|
33
33
|
|
|
34
34
|
* **Tu código (Cliente):** `User.create(name: 'Gabi')`
|
|
35
35
|
* **Protocolo (BugBunny):** Envía `POST /users` (Header `type: users`) vía RabbitMQ.
|
|
36
|
-
* **Worker (Servidor):** Recibe el mensaje y ejecuta `UsersController#create`.
|
|
36
|
+
* **Worker (Servidor):** Recibe el mensaje, evalúa tu mapa de rutas y ejecuta `UsersController#create`.
|
|
37
37
|
|
|
38
38
|
---
|
|
39
39
|
|
|
@@ -42,7 +42,7 @@ En lugar de pensar en "Exchanges" y "Queues", BugBunny inyecta verbos HTTP (`GET
|
|
|
42
42
|
Agrega la gema a tu `Gemfile`:
|
|
43
43
|
|
|
44
44
|
```ruby
|
|
45
|
-
gem 'bug_bunny', '~>
|
|
45
|
+
gem 'bug_bunny', '~> 4.0'
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
Ejecuta el bundle e instala los archivos base:
|
|
@@ -95,9 +95,9 @@ BugBunny::Resource.connection_pool = BUG_BUNNY_POOL
|
|
|
95
95
|
|
|
96
96
|
---
|
|
97
97
|
|
|
98
|
-
## 🏗️ Configuración de Infraestructura en Cascada
|
|
98
|
+
## 🏗️ Configuración de Infraestructura en Cascada
|
|
99
99
|
|
|
100
|
-
BugBunny
|
|
100
|
+
BugBunny utiliza un sistema de configuración jerárquico para los parámetros de RabbitMQ (como la durabilidad de Exchanges y Colas). Las opciones se resuelven en el siguiente orden de prioridad:
|
|
101
101
|
|
|
102
102
|
1. **Defaults de la Gema:** Rápidos y efímeros (`durable: false`).
|
|
103
103
|
2. **Configuración Global:** Definida en el inicializador para todo el entorno.
|
|
@@ -124,7 +124,7 @@ end
|
|
|
124
124
|
Los recursos son proxies de servicios remotos. Heredan de `BugBunny::Resource`.
|
|
125
125
|
|
|
126
126
|
### 1. Definición y Atributos Híbridos
|
|
127
|
-
BugBunny
|
|
127
|
+
BugBunny es **Schema-less**. Soporta atributos tipados (ActiveModel) y dinámicos simultáneamente, además de definir su propia infraestructura.
|
|
128
128
|
|
|
129
129
|
```ruby
|
|
130
130
|
# app/models/manager/service.rb
|
|
@@ -188,7 +188,7 @@ Manager::Service.with(
|
|
|
188
188
|
```
|
|
189
189
|
|
|
190
190
|
### 4. Client Middleware (Interceptores)
|
|
191
|
-
Intercepta peticiones de ida y respuestas de vuelta en la arquitectura del cliente.
|
|
191
|
+
Intercepta peticiones de ida y respuestas de vuelta en la arquitectura del cliente.
|
|
192
192
|
|
|
193
193
|
**Middlewares Incluidos (Built-ins)**
|
|
194
194
|
Si usas `BugBunny::Resource`, el manejo de JSON y de errores ya está integrado automáticamente. Pero si utilizas el cliente manual (`BugBunny::Client`), puedes inyectar los middlewares incluidos para no tener que parsear respuestas manualmente:
|
|
@@ -251,22 +251,44 @@ BugBunny::Middleware::RaiseError.prepend(CustomBugBunnyErrors)
|
|
|
251
251
|
|
|
252
252
|
## 📡 Modo Servidor: Controladores
|
|
253
253
|
|
|
254
|
-
BugBunny
|
|
254
|
+
A partir de BugBunny v4, el enrutamiento es **declarativo** y explícito, al igual que en Rails. Se utiliza un archivo de rutas centralizado para mapear los mensajes AMQP entrantes a los Controladores adecuados.
|
|
255
255
|
|
|
256
|
-
### 1. Ruteo
|
|
257
|
-
|
|
256
|
+
### 1. Ruteo Declarativo (Rutas)
|
|
257
|
+
Crea un inicializador en tu aplicación (ej. `config/initializers/bug_bunny_routes.rb`) para definir tu mapa de rutas. BugBunny usará este DSL para extraer automáticamente parámetros dinámicos de las URLs.
|
|
258
258
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
259
|
+
```ruby
|
|
260
|
+
# config/initializers/bug_bunny_routes.rb
|
|
261
|
+
|
|
262
|
+
BugBunny.routes.draw do
|
|
263
|
+
# 1. Colecciones Básicas y Filtrado
|
|
264
|
+
# Genera rutas para index, show y update únicamente
|
|
265
|
+
resources :services, only: [:index, :show, :update]
|
|
266
|
+
|
|
267
|
+
# 2. Rutas Anidadas (Member y Collection)
|
|
268
|
+
resources :nodes, except: [:create, :destroy] do
|
|
269
|
+
# Member inyecta el parámetro :id (ej. PUT nodes/:id/drain)
|
|
270
|
+
member do
|
|
271
|
+
put :drain
|
|
272
|
+
post :restart
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Collection opera sobre el conjunto (ej. GET nodes/stats)
|
|
276
|
+
collection do
|
|
277
|
+
get :stats
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# 3. Rutas estáticas (Colecciones o Acciones Custom)
|
|
282
|
+
get 'health_checks/up', to: 'health_checks#up'
|
|
283
|
+
|
|
284
|
+
# 4. Extracción automática de variables dinámicas profundas
|
|
285
|
+
get 'api/v1/clusters/:cluster_id/nodes/:node_id/metrics', to: 'api/v1/metrics#show'
|
|
286
|
+
end
|
|
287
|
+
```
|
|
267
288
|
|
|
268
289
|
### 2. El Controlador
|
|
269
290
|
Ubicación: `app/rabbit/controllers/`.
|
|
291
|
+
Los parámetros declarados en el archivo de rutas (como `:id` o `:cluster_id`) estarán disponibles automáticamente dentro del hash `params` de tu controlador.
|
|
270
292
|
|
|
271
293
|
```ruby
|
|
272
294
|
class ServicesController < BugBunny::Controller
|
|
@@ -279,7 +301,7 @@ class ServicesController < BugBunny::Controller
|
|
|
279
301
|
end
|
|
280
302
|
|
|
281
303
|
def create
|
|
282
|
-
# BugBunny envuelve los params automáticamente
|
|
304
|
+
# BugBunny envuelve los params automáticamente basándose en el resource_name
|
|
283
305
|
# params[:service] => { name: '...', replicas: ... }
|
|
284
306
|
if Service.create(params[:service])
|
|
285
307
|
render status: 201, json: { status: 'created' }
|
|
@@ -291,7 +313,7 @@ class ServicesController < BugBunny::Controller
|
|
|
291
313
|
private
|
|
292
314
|
|
|
293
315
|
def set_service
|
|
294
|
-
# params[:id]
|
|
316
|
+
# params[:id] es extraído e inyectado por el BugBunny.routes
|
|
295
317
|
@service = Service.find(params[:id])
|
|
296
318
|
end
|
|
297
319
|
end
|
|
@@ -317,16 +339,14 @@ end
|
|
|
317
339
|
|
|
318
340
|
## 🔎 Observabilidad y Tracing
|
|
319
341
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
El `correlation_id` se mantiene intacto a través de toda la cadena: `Producer -> RabbitMQ -> Consumer -> Controller`.
|
|
342
|
+
BugBunny implementa Distributed Tracing nativo. El `correlation_id` se mantiene intacto a través de toda la cadena: `Producer -> RabbitMQ -> Consumer -> Controller`.
|
|
323
343
|
|
|
324
344
|
### 1. Logs Automáticos (Consumer)
|
|
325
345
|
No requiere configuración. El worker envuelve la ejecución en bloques de log etiquetados con el UUID.
|
|
326
346
|
|
|
327
347
|
```text
|
|
328
|
-
[d41d8cd9...] [Consumer]
|
|
329
|
-
[d41d8cd9...] [
|
|
348
|
+
[d41d8cd9...] [BugBunny::Consumer] 📥 Received PUT "/nodes/4bv445vgc158hk" | RK: 'dbu55...'
|
|
349
|
+
[d41d8cd9...] [BugBunny::Consumer] 🎯 Routed to Rabbit::Controllers::NodesController#drain
|
|
330
350
|
```
|
|
331
351
|
|
|
332
352
|
### 2. Logs de Negocio (Controller)
|
|
@@ -378,7 +398,7 @@ Para máxima velocidad, BugBunny usa `amq.rabbitmq.reply-to`.
|
|
|
378
398
|
* **Recomendación:** Diseña tus acciones RPC (`POST`, `PUT`) para que sean **idempotentes** (seguras de reintentar ante un timeout).
|
|
379
399
|
|
|
380
400
|
### Seguridad
|
|
381
|
-
El Router incluye protecciones contra **Remote Code Execution (RCE)**.
|
|
401
|
+
El Router incluye protecciones contra **Remote Code Execution (RCE)**. El Consumer verifica estrictamente que el Controlador resuelto a través del archivo de rutas herede de `BugBunny::Controller` antes de ejecutarla, impidiendo la inyección de clases arbitrarias. Además, las llamadas a rutas no registradas fallan rápido con un `404 Not Found`.
|
|
382
402
|
|
|
383
403
|
### Health Checks en Docker Swarm / Kubernetes
|
|
384
404
|
Dado que un Worker se ejecuta en segundo plano sin exponer un servidor web tradicional, orquestadores como Docker Swarm o Kubernetes no pueden usar un endpoint HTTP para verificar si el proceso está saludable.
|
data/lib/bug_bunny/consumer.rb
CHANGED
|
@@ -15,7 +15,7 @@ module BugBunny
|
|
|
15
15
|
# Sus responsabilidades son:
|
|
16
16
|
# 1. Escuchar una cola específica.
|
|
17
17
|
# 2. Deserializar el mensaje y sus headers.
|
|
18
|
-
# 3.
|
|
18
|
+
# 3. Consultar el mapa global `BugBunny.routes` para enrutar el mensaje a un Controlador.
|
|
19
19
|
# 4. Gestionar el ciclo de respuesta RPC (Request-Response) para evitar timeouts en el cliente.
|
|
20
20
|
#
|
|
21
21
|
# @example Suscripción manual
|
|
@@ -99,16 +99,16 @@ module BugBunny
|
|
|
99
99
|
|
|
100
100
|
private
|
|
101
101
|
|
|
102
|
-
# Procesa un mensaje individual recibido de la cola.
|
|
102
|
+
# Procesa un mensaje individual recibido de la cola orquestando el ruteo declarativo.
|
|
103
103
|
#
|
|
104
|
-
# Realiza la orquestación completa: Parsing ->
|
|
104
|
+
# Realiza la orquestación completa: Parsing -> Reconocimiento de Ruta -> Ejecución -> Respuesta.
|
|
105
105
|
#
|
|
106
106
|
# @param delivery_info [Bunny::DeliveryInfo] Metadatos de entrega (tag, redelivered, etc).
|
|
107
107
|
# @param properties [Bunny::MessageProperties] Headers y propiedades AMQP (reply_to, correlation_id).
|
|
108
108
|
# @param body [String] El payload crudo del mensaje.
|
|
109
109
|
# @return [void]
|
|
110
110
|
def process_message(delivery_info, properties, body)
|
|
111
|
-
# 1. Validación de Headers
|
|
111
|
+
# 1. Validación de Headers (URL path)
|
|
112
112
|
path = properties.type || (properties.headers && properties.headers['path'])
|
|
113
113
|
|
|
114
114
|
if path.nil? || path.empty?
|
|
@@ -119,142 +119,91 @@ module BugBunny
|
|
|
119
119
|
|
|
120
120
|
# 2. Recuperación Robusta del Verbo HTTP
|
|
121
121
|
headers_hash = properties.headers || {}
|
|
122
|
-
http_method = headers_hash['x-http-method'] || headers_hash['method'] || 'GET'
|
|
122
|
+
http_method = (headers_hash['x-http-method'] || headers_hash['method'] || 'GET').to_s.upcase
|
|
123
123
|
|
|
124
|
-
|
|
125
|
-
route_info = router_dispatch(http_method, path)
|
|
126
|
-
|
|
127
|
-
BugBunny.configuration.logger.info("[BugBunny::Consumer] 📥 Started #{http_method} \"/#{path}\" for Routing Key: #{delivery_info.routing_key}")
|
|
124
|
+
BugBunny.configuration.logger.info("[BugBunny::Consumer] 📥 Received #{http_method} \"/#{path}\" | RK: '#{delivery_info.routing_key}'")
|
|
128
125
|
BugBunny.configuration.logger.debug("[BugBunny::Consumer] 📦 Body: #{body.truncate(200)}")
|
|
129
126
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
action: route_info[:action],
|
|
135
|
-
id: route_info[:id],
|
|
136
|
-
query_params: route_info[:params],
|
|
137
|
-
content_type: properties.content_type,
|
|
138
|
-
correlation_id: properties.correlation_id,
|
|
139
|
-
reply_to: properties.reply_to
|
|
140
|
-
}.merge(properties.headers)
|
|
127
|
+
# ===================================================================
|
|
128
|
+
# 3. Ruteo Declarativo
|
|
129
|
+
# ===================================================================
|
|
130
|
+
uri = URI.parse("http://dummy/#{path}")
|
|
141
131
|
|
|
142
|
-
#
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
132
|
+
# Extraemos query params (ej. /nodes?status=active)
|
|
133
|
+
query_params = uri.query ? Rack::Utils.parse_nested_query(uri.query) : {}
|
|
134
|
+
if defined?(ActiveSupport::HashWithIndifferentAccess)
|
|
135
|
+
query_params = query_params.with_indifferent_access
|
|
136
|
+
end
|
|
147
137
|
|
|
148
|
-
|
|
149
|
-
|
|
138
|
+
# Le preguntamos al motor de rutas global quién debe manejar esto
|
|
139
|
+
route_info = BugBunny.routes.recognize(http_method, uri.path)
|
|
150
140
|
|
|
151
|
-
|
|
141
|
+
if route_info.nil?
|
|
142
|
+
BugBunny.configuration.logger.warn("[BugBunny::Consumer] ⚠️ No route matches [#{http_method}] \"/#{uri.path}\"")
|
|
143
|
+
handle_fatal_error(properties, 404, "Not Found", "No route matches [#{http_method}] \"/#{uri.path}\"")
|
|
144
|
+
session.channel.reject(delivery_info.delivery_tag, false)
|
|
145
|
+
return
|
|
146
|
+
end
|
|
152
147
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
148
|
+
# Fusionamos los parámetros extraídos de la URL (ej. :id) con los query_params
|
|
149
|
+
final_params = query_params.merge(route_info[:params])
|
|
150
|
+
|
|
151
|
+
# ===================================================================
|
|
152
|
+
# 4. Instanciación del Controlador
|
|
153
|
+
# ===================================================================
|
|
154
|
+
namespace = BugBunny.configuration.controller_namespace
|
|
155
|
+
controller_name = route_info[:controller].camelize
|
|
156
|
+
controller_class_name = "#{namespace}::#{controller_name}Controller"
|
|
157
|
+
|
|
158
|
+
begin
|
|
159
|
+
controller_class = controller_class_name.constantize
|
|
160
|
+
rescue NameError
|
|
161
|
+
BugBunny.configuration.logger.warn("[BugBunny::Consumer] ⚠️ Controller class not found: #{controller_class_name}")
|
|
158
162
|
handle_fatal_error(properties, 404, "Not Found", "Controller #{controller_class_name} not found")
|
|
159
163
|
session.channel.reject(delivery_info.delivery_tag, false)
|
|
160
164
|
return
|
|
161
165
|
end
|
|
162
166
|
|
|
163
|
-
#
|
|
167
|
+
# Verificación estricta de Seguridad (RCE Prevention)
|
|
168
|
+
unless controller_class < BugBunny::Controller
|
|
169
|
+
BugBunny.configuration.logger.error("[BugBunny::Consumer] ⛔ Security Alert: #{controller_class} is not a valid BugBunny Controller")
|
|
170
|
+
handle_fatal_error(properties, 403, "Forbidden", "Invalid Controller Class")
|
|
171
|
+
session.channel.reject(delivery_info.delivery_tag, false)
|
|
172
|
+
return
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
BugBunny.configuration.logger.debug("[BugBunny::Consumer] 🎯 Routed to #{controller_class_name}##{route_info[:action]}")
|
|
176
|
+
|
|
177
|
+
request_metadata = {
|
|
178
|
+
type: path,
|
|
179
|
+
http_method: http_method,
|
|
180
|
+
controller: route_info[:controller],
|
|
181
|
+
action: route_info[:action],
|
|
182
|
+
id: final_params['id'] || final_params[:id],
|
|
183
|
+
query_params: final_params,
|
|
184
|
+
content_type: properties.content_type,
|
|
185
|
+
correlation_id: properties.correlation_id,
|
|
186
|
+
reply_to: properties.reply_to
|
|
187
|
+
}.merge(headers_hash)
|
|
188
|
+
|
|
189
|
+
# ===================================================================
|
|
190
|
+
# 5. Ejecución y Respuesta
|
|
191
|
+
# ===================================================================
|
|
164
192
|
response_payload = controller_class.call(headers: request_metadata, body: body)
|
|
165
193
|
|
|
166
|
-
# 6. Respuesta RPC
|
|
167
194
|
if properties.reply_to
|
|
168
195
|
reply(response_payload, properties.reply_to, properties.correlation_id)
|
|
169
196
|
end
|
|
170
197
|
|
|
171
|
-
# 7. Acknowledge
|
|
172
198
|
session.channel.ack(delivery_info.delivery_tag)
|
|
173
199
|
|
|
174
200
|
rescue StandardError => e
|
|
175
201
|
BugBunny.configuration.logger.error("[BugBunny::Consumer] 💥 Execution Error (#{e.class}): #{e.message}")
|
|
202
|
+
BugBunny.configuration.logger.debug(e.backtrace.join("\n"))
|
|
176
203
|
handle_fatal_error(properties, 500, "Internal Server Error", e.message)
|
|
177
204
|
session.channel.reject(delivery_info.delivery_tag, false)
|
|
178
205
|
end
|
|
179
206
|
|
|
180
|
-
# Interpreta la URL y el verbo para decidir qué controlador ejecutar.
|
|
181
|
-
#
|
|
182
|
-
# Implementa un Router Heurístico que soporta namespaces y acciones custom
|
|
183
|
-
# buscando dinámicamente el ID en la ruta mediante Regex y Fallback Semántico.
|
|
184
|
-
#
|
|
185
|
-
# @param method [String] Verbo HTTP (GET, POST, etc).
|
|
186
|
-
# @param path [String] URL virtual del recurso (ej: 'foo/bar/algo/13/test').
|
|
187
|
-
# @return [Hash] Estructura con keys {:controller, :action, :id, :params}.
|
|
188
|
-
def router_dispatch(method, path)
|
|
189
|
-
uri = URI.parse("http://dummy/#{path}")
|
|
190
|
-
segments = uri.path.split('/').reject(&:empty?)
|
|
191
|
-
|
|
192
|
-
query_params = uri.query ? Rack::Utils.parse_nested_query(uri.query) : {}
|
|
193
|
-
if defined?(ActiveSupport::HashWithIndifferentAccess)
|
|
194
|
-
query_params = query_params.with_indifferent_access
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
# 1. Acción Built-in: Health Check Global (/up o /api/up)
|
|
198
|
-
if segments.last == 'up' && method.to_s.upcase == 'GET'
|
|
199
|
-
ctrl = segments.size > 1 ? segments[0...-1].join('/') : 'application'
|
|
200
|
-
return { controller: ctrl, action: 'up', id: nil, params: query_params }
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
# 2. Búsqueda dinámica del ID (Heurística por Regex)
|
|
204
|
-
# Patrón: Enteros, UUIDs, o Hashes largos (Docker Swarm 25 chars, Mongo 24 chars)
|
|
205
|
-
id_pattern = /^(?:\d+|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}|[a-zA-Z0-9_-]{20,})$/
|
|
206
|
-
|
|
207
|
-
# FIX: Usamos rindex (de derecha a izquierda) para evitar falsos positivos con namespaces como 'v1'
|
|
208
|
-
id_index = segments.rindex { |s| s.match?(id_pattern) }
|
|
209
|
-
|
|
210
|
-
# 3. Fallback Semántico Posicional
|
|
211
|
-
# Si el regex no detectó el ID (ej: ID corto como "node-1"), pero la semántica HTTP
|
|
212
|
-
# indica que es una operación singular (PUT/DELETE/GET), asumimos que el último segmento es el ID.
|
|
213
|
-
if id_index.nil? && segments.size >= 2
|
|
214
|
-
last_segment = segments.last
|
|
215
|
-
method_up = method.to_s.upcase
|
|
216
|
-
|
|
217
|
-
is_member_verb = %w[PUT PATCH DELETE].include?(method_up)
|
|
218
|
-
# En GET, nos aseguramos que la última palabra no sea una acción estándar de REST
|
|
219
|
-
is_get_member = method_up == 'GET' && !%w[index new edit up action].include?(last_segment)
|
|
220
|
-
|
|
221
|
-
if is_member_verb || is_get_member
|
|
222
|
-
# Si tiene 3 o más segmentos (ej. nodes/node-1/stats), el ID no está al final.
|
|
223
|
-
# Este fallback asume que para IDs raros, el formato clásico es recurso/id
|
|
224
|
-
id_index = segments.size - 1
|
|
225
|
-
end
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
# 4. Asignación de variables según escenario
|
|
229
|
-
if id_index
|
|
230
|
-
# ESCENARIO A: Ruta Miembro (ej. nodes/4bv445vgc158hk4twlxmdjo0v/stats)
|
|
231
|
-
controller_name = segments[0...id_index].join('/')
|
|
232
|
-
id = segments[id_index]
|
|
233
|
-
action = segments[id_index + 1] # Puede ser nil si no hay acción extra al final
|
|
234
|
-
else
|
|
235
|
-
# ESCENARIO B: Ruta Colección (ej. api/v1/nodes)
|
|
236
|
-
controller_name = segments.join('/')
|
|
237
|
-
id = nil
|
|
238
|
-
action = nil
|
|
239
|
-
end
|
|
240
|
-
|
|
241
|
-
# 5. Inferimos la acción clásica de Rails si no hay una explícita
|
|
242
|
-
unless action
|
|
243
|
-
action = case method.to_s.upcase
|
|
244
|
-
when 'GET' then id ? 'show' : 'index'
|
|
245
|
-
when 'POST' then 'create'
|
|
246
|
-
when 'PUT', 'PATCH' then 'update'
|
|
247
|
-
when 'DELETE' then 'destroy'
|
|
248
|
-
else id ? 'show' : 'index'
|
|
249
|
-
end
|
|
250
|
-
end
|
|
251
|
-
|
|
252
|
-
# 6. Inyectamos el ID en los parámetros para fácil acceso en el Controlador
|
|
253
|
-
query_params['id'] = id if id
|
|
254
|
-
|
|
255
|
-
{ controller: controller_name, action: action, id: id, params: query_params }
|
|
256
|
-
end
|
|
257
|
-
|
|
258
207
|
# Envía una respuesta al cliente RPC utilizando Direct Reply-to.
|
|
259
208
|
#
|
|
260
209
|
# @param payload [Hash] Cuerpo de la respuesta ({ status: ..., body: ... }).
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BugBunny
|
|
4
|
+
module Routing
|
|
5
|
+
# Representa una ruta individual dentro del mapa de rutas de BugBunny.
|
|
6
|
+
#
|
|
7
|
+
# Esta clase se encarga de compilar un patrón de URL (ej. 'users/:id') en una
|
|
8
|
+
# Expresión Regular capaz de evaluar coincidencias y extraer los parámetros
|
|
9
|
+
# dinámicos nombrados de forma automática.
|
|
10
|
+
class Route
|
|
11
|
+
# @return [String] El verbo HTTP de la ruta (GET, POST, PUT, DELETE).
|
|
12
|
+
attr_reader :http_method
|
|
13
|
+
|
|
14
|
+
# @return [String] El patrón original de la ruta (ej. 'nodes/:node_id/metrics').
|
|
15
|
+
attr_reader :path_pattern
|
|
16
|
+
|
|
17
|
+
# @return [String] El nombre del controlador en formato snake_case (ej. 'api/v1/metrics').
|
|
18
|
+
attr_reader :controller
|
|
19
|
+
|
|
20
|
+
# @return [String] El nombre de la acción a ejecutar (ej. 'show').
|
|
21
|
+
attr_reader :action
|
|
22
|
+
|
|
23
|
+
# Inicializa una nueva Ruta compilando su Expresión Regular.
|
|
24
|
+
#
|
|
25
|
+
# @param http_method [String, Symbol] Verbo HTTP (ej. :get, 'POST').
|
|
26
|
+
# @param path_pattern [String] Patrón de la URL. Los parámetros dinámicos deben iniciar con ':' (ej. 'users/:id').
|
|
27
|
+
# @param to [String] Destino en formato 'controlador#accion' (ej. 'users#show').
|
|
28
|
+
# @raise [ArgumentError] Si el formato del destino `to` es inválido.
|
|
29
|
+
def initialize(http_method, path_pattern, to:)
|
|
30
|
+
@http_method = http_method.to_s.upcase
|
|
31
|
+
@path_pattern = normalize_path(path_pattern)
|
|
32
|
+
|
|
33
|
+
parse_destination!(to)
|
|
34
|
+
compile_regex!
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Evalúa si una petición entrante coincide con esta ruta.
|
|
38
|
+
#
|
|
39
|
+
# @param method [String] El verbo HTTP entrante.
|
|
40
|
+
# @param path [String] La URL entrante a evaluar.
|
|
41
|
+
# @return [Boolean] `true` si hace match, `false` en caso contrario.
|
|
42
|
+
def match?(method, path)
|
|
43
|
+
return false unless @http_method == method.to_s.upcase
|
|
44
|
+
|
|
45
|
+
normalized_path = normalize_path(path)
|
|
46
|
+
@regex.match?(normalized_path)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Extrae los parámetros dinámicos de una URL que hizo coincidencia con el patrón.
|
|
50
|
+
#
|
|
51
|
+
# @param path [String] La URL entrante (ej. 'users/123').
|
|
52
|
+
# @return [Hash] Diccionario con las variables extraídas (ej. { 'id' => '123' }).
|
|
53
|
+
# @example
|
|
54
|
+
# route = Route.new('GET', 'users/:id', to: 'users#show')
|
|
55
|
+
# route.extract_params('users/42') # => { 'id' => '42' }
|
|
56
|
+
def extract_params(path)
|
|
57
|
+
normalized_path = normalize_path(path)
|
|
58
|
+
match_data = @regex.match(normalized_path)
|
|
59
|
+
|
|
60
|
+
return {} unless match_data
|
|
61
|
+
|
|
62
|
+
# match_data.named_captures devuelve un Hash con las variables que definimos en la Regex
|
|
63
|
+
match_data.named_captures
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
# Elimina las barras '/' al principio y al final para evitar problemas de formato.
|
|
69
|
+
#
|
|
70
|
+
# @param path [String] URL cruda.
|
|
71
|
+
# @return [String] URL normalizada.
|
|
72
|
+
def normalize_path(path)
|
|
73
|
+
path.to_s.gsub(%r{^/|/$}, '')
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Parsea el string 'controlador#accion' y lo asigna a las variables de instancia.
|
|
77
|
+
#
|
|
78
|
+
# @param destination [String] El destino declarado por el usuario.
|
|
79
|
+
def parse_destination!(destination)
|
|
80
|
+
parts = destination.split('#')
|
|
81
|
+
if parts.size != 2
|
|
82
|
+
raise ArgumentError, "Destino inválido: '#{destination}'. Debe seguir el formato 'controlador#accion'."
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
@controller = parts[0]
|
|
86
|
+
@action = parts[1]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Transforma el string 'users/:id' en una Regex de Ruby con "Named Captures".
|
|
90
|
+
# Reemplaza los :param por (?<param>[^/]+) que captura todo hasta la siguiente barra.
|
|
91
|
+
def compile_regex!
|
|
92
|
+
# Si la ruta es estática ('swarm/info'), la regex simplemente será /^swarm\/info$/
|
|
93
|
+
# Si tiene variables ('nodes/:id'), convertimos el :id en un grupo de captura.
|
|
94
|
+
pattern = @path_pattern.gsub(/:([a-zA-Z0-9_]+)/) do |match|
|
|
95
|
+
param_name = match.delete(':')
|
|
96
|
+
"(?<#{param_name}>[^/]+)"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
@regex = Regexp.new("^#{pattern}$")
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'route'
|
|
4
|
+
|
|
5
|
+
module BugBunny
|
|
6
|
+
module Routing
|
|
7
|
+
# Gestiona la colección de rutas de la aplicación y expone el DSL de configuración.
|
|
8
|
+
#
|
|
9
|
+
# Actúa como el motor principal del enrutador. Permite definir rutas de forma
|
|
10
|
+
# declarativa (estilo Rails) e incluye macros convenientes como `resources`,
|
|
11
|
+
# soportando bloques anidados `member` y `collection`.
|
|
12
|
+
#
|
|
13
|
+
# @example Configuración del DSL en un inicializador
|
|
14
|
+
# route_set = RouteSet.new
|
|
15
|
+
# route_set.draw do
|
|
16
|
+
# get 'ping', to: 'health#ping'
|
|
17
|
+
#
|
|
18
|
+
# resources :nodes, except: [:create, :destroy] do
|
|
19
|
+
# member do
|
|
20
|
+
# put :drain
|
|
21
|
+
# end
|
|
22
|
+
# collection do
|
|
23
|
+
# get :stats
|
|
24
|
+
# end
|
|
25
|
+
# end
|
|
26
|
+
# end
|
|
27
|
+
class RouteSet
|
|
28
|
+
# @return [Array<BugBunny::Routing::Route>] Lista de rutas registradas y compiladas.
|
|
29
|
+
attr_reader :routes
|
|
30
|
+
|
|
31
|
+
# Inicializa un conjunto de rutas vacío y prepara el stack de scopes.
|
|
32
|
+
def initialize
|
|
33
|
+
@routes = []
|
|
34
|
+
@scopes = [] # Pila para rastrear el contexto (namespaces, resources, members)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Evalúa un bloque de código en el contexto de esta instancia para construir el mapa.
|
|
38
|
+
# Utiliza `instance_eval` para exponer el DSL directamente.
|
|
39
|
+
#
|
|
40
|
+
# @yield Bloque de configuración conteniendo las definiciones de ruteo.
|
|
41
|
+
# @return [void]
|
|
42
|
+
def draw(&block)
|
|
43
|
+
instance_eval(&block)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Borra todas las rutas registradas y limpia los scopes.
|
|
47
|
+
# Es útil para entornos de pruebas (testing) o recarga en caliente (hot-reloading).
|
|
48
|
+
#
|
|
49
|
+
# @return [void]
|
|
50
|
+
def clear!
|
|
51
|
+
@routes.clear
|
|
52
|
+
@scopes.clear
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @!group DSL de Verbos HTTP
|
|
56
|
+
|
|
57
|
+
# Registra una ruta para el verbo GET.
|
|
58
|
+
# @param path [String, Symbol] Patrón de la URL.
|
|
59
|
+
# @param to [String, nil] Destino (controlador#accion). Si es nil, se infiere del scope.
|
|
60
|
+
def get(path, to: nil); add_route('GET', path, to: to); end
|
|
61
|
+
|
|
62
|
+
# Registra una ruta para el verbo POST.
|
|
63
|
+
# @param path [String, Symbol] Patrón de la URL.
|
|
64
|
+
# @param to [String, nil] Destino (controlador#accion). Si es nil, se infiere del scope.
|
|
65
|
+
def post(path, to: nil); add_route('POST', path, to: to); end
|
|
66
|
+
|
|
67
|
+
# Registra una ruta para el verbo PUT.
|
|
68
|
+
# @param path [String, Symbol] Patrón de la URL.
|
|
69
|
+
# @param to [String, nil] Destino (controlador#accion). Si es nil, se infiere del scope.
|
|
70
|
+
def put(path, to: nil); add_route('PUT', path, to: to); end
|
|
71
|
+
|
|
72
|
+
# Registra una ruta para el verbo PATCH.
|
|
73
|
+
# @param path [String, Symbol] Patrón de la URL.
|
|
74
|
+
# @param to [String, nil] Destino (controlador#accion). Si es nil, se infiere del scope.
|
|
75
|
+
def patch(path, to: nil); add_route('PATCH', path, to: to); end
|
|
76
|
+
|
|
77
|
+
# Registra una ruta para el verbo DELETE.
|
|
78
|
+
# @param path [String, Symbol] Patrón de la URL.
|
|
79
|
+
# @param to [String, nil] Destino (controlador#accion). Si es nil, se infiere del scope.
|
|
80
|
+
def delete(path, to: nil); add_route('DELETE', path, to: to); end
|
|
81
|
+
|
|
82
|
+
# @!endgroup
|
|
83
|
+
|
|
84
|
+
# Macro que genera automáticamente las rutas CRUD para un recurso RESTful.
|
|
85
|
+
# Soporta filtrado mediante `only` y `except`, y acepta un bloque para rutas anidadas.
|
|
86
|
+
#
|
|
87
|
+
# Mapea las acciones: index (GET), show (GET /:id), create (POST),
|
|
88
|
+
# update (PUT/PATCH /:id) y destroy (DELETE /:id).
|
|
89
|
+
#
|
|
90
|
+
# @param name [Symbol, String] Nombre del recurso en plural (ej. :nodes).
|
|
91
|
+
# @param only [Array<Symbol>, Symbol, nil] Acciones a incluir exclusivamente.
|
|
92
|
+
# @param except [Array<Symbol>, Symbol, nil] Acciones a excluir.
|
|
93
|
+
# @yield Bloque para definir rutas `member` o `collection`.
|
|
94
|
+
# @return [void]
|
|
95
|
+
def resources(name, only: nil, except: nil, &block)
|
|
96
|
+
resource_path = name.to_s
|
|
97
|
+
|
|
98
|
+
# Todas las acciones estándar disponibles
|
|
99
|
+
actions = [:index, :show, :create, :update, :destroy]
|
|
100
|
+
|
|
101
|
+
# Aplicamos los filtros si existen
|
|
102
|
+
if only
|
|
103
|
+
actions &= Array(only).map(&:to_sym)
|
|
104
|
+
elsif except
|
|
105
|
+
actions -= Array(except).map(&:to_sym)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Rutas estándar (Fuera del scope anidado)
|
|
109
|
+
get resource_path, to: "#{resource_path}#index" if actions.include?(:index)
|
|
110
|
+
post resource_path, to: "#{resource_path}#create" if actions.include?(:create)
|
|
111
|
+
get "#{resource_path}/:id", to: "#{resource_path}#show" if actions.include?(:show)
|
|
112
|
+
put "#{resource_path}/:id", to: "#{resource_path}#update" if actions.include?(:update)
|
|
113
|
+
patch "#{resource_path}/:id", to: "#{resource_path}#update" if actions.include?(:update)
|
|
114
|
+
delete "#{resource_path}/:id", to: "#{resource_path}#destroy" if actions.include?(:destroy)
|
|
115
|
+
|
|
116
|
+
# Si se pasa un bloque, abrimos un Scope de Recurso para rutas anidadas
|
|
117
|
+
if block_given?
|
|
118
|
+
with_scope({ type: :resource, name: resource_path }) do
|
|
119
|
+
instance_eval(&block)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Define rutas aplicables a un miembro específico del recurso (Requieren un ID).
|
|
125
|
+
#
|
|
126
|
+
# Al usar este bloque, el router antepondrá automáticamente el nombre del recurso
|
|
127
|
+
# y el parámetro `:id` a la URL generada, e inferirá el controlador base.
|
|
128
|
+
#
|
|
129
|
+
# @yield Bloque conteniendo definiciones de rutas.
|
|
130
|
+
# @raise [ArgumentError] Si se llama fuera de un bloque `resources`.
|
|
131
|
+
# @return [void]
|
|
132
|
+
# @example
|
|
133
|
+
# resources :nodes do
|
|
134
|
+
# member do
|
|
135
|
+
# put :drain # Genera: PUT nodes/:id/drain => NodesController#drain
|
|
136
|
+
# end
|
|
137
|
+
# end
|
|
138
|
+
def member(&block)
|
|
139
|
+
unless current_scope[:type] == :resource
|
|
140
|
+
raise ArgumentError, "El bloque 'member' solo puede usarse dentro de un bloque 'resources'"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
with_scope({ type: :member }) do
|
|
144
|
+
instance_eval(&block)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Define rutas aplicables a la colección completa del recurso (Sin ID).
|
|
149
|
+
#
|
|
150
|
+
# Al usar este bloque, el router antepondrá automáticamente el nombre del recurso
|
|
151
|
+
# a la URL generada e inferirá el controlador base.
|
|
152
|
+
#
|
|
153
|
+
# @yield Bloque conteniendo definiciones de rutas.
|
|
154
|
+
# @raise [ArgumentError] Si se llama fuera de un bloque `resources`.
|
|
155
|
+
# @return [void]
|
|
156
|
+
# @example
|
|
157
|
+
# resources :nodes do
|
|
158
|
+
# collection do
|
|
159
|
+
# get :stats # Genera: GET nodes/stats => NodesController#stats
|
|
160
|
+
# end
|
|
161
|
+
# end
|
|
162
|
+
def collection(&block)
|
|
163
|
+
unless current_scope[:type] == :resource
|
|
164
|
+
raise ArgumentError, "El bloque 'collection' solo puede usarse dentro de un bloque 'resources'"
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
with_scope({ type: :collection }) do
|
|
168
|
+
instance_eval(&block)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Evalúa una petición entrante contra el mapa de rutas.
|
|
173
|
+
#
|
|
174
|
+
# Recorre las rutas en el orden en que fueron definidas. La primera ruta que
|
|
175
|
+
# haga match será la ganadora. Retorna los datos necesarios para instanciar
|
|
176
|
+
# el controlador e inyectarle los parámetros dinámicos extraídos.
|
|
177
|
+
#
|
|
178
|
+
# @param method [String] Verbo HTTP entrante (ej. 'GET').
|
|
179
|
+
# @param path [String] URL entrante (ej. 'nodes/123/drain').
|
|
180
|
+
# @return [Hash, nil] Hash con `:controller`, `:action` y `:params`, o `nil` si no hay match.
|
|
181
|
+
def recognize(method, path)
|
|
182
|
+
@routes.each do |route|
|
|
183
|
+
if route.match?(method, path)
|
|
184
|
+
extracted_params = route.extract_params(path)
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
controller: route.controller,
|
|
188
|
+
action: route.action,
|
|
189
|
+
params: extracted_params
|
|
190
|
+
}
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Si llegamos aquí, es un 404 seguro.
|
|
195
|
+
nil
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
private
|
|
199
|
+
|
|
200
|
+
# Instancia y almacena la ruta resolviendo la URL final y el controlador según el scope.
|
|
201
|
+
#
|
|
202
|
+
# @param method [String] Verbo HTTP.
|
|
203
|
+
# @param path [String, Symbol] Ruta declarada.
|
|
204
|
+
# @param to [String, nil] Destino declarado.
|
|
205
|
+
# @raise [ArgumentError] Si no se puede inferir el destino y no se provee uno.
|
|
206
|
+
# @api private
|
|
207
|
+
def add_route(method, path, to: nil)
|
|
208
|
+
final_path = path.to_s
|
|
209
|
+
final_to = to
|
|
210
|
+
|
|
211
|
+
# Inferimos rutas basadas en el Scope Activo
|
|
212
|
+
if in_scope?(:member)
|
|
213
|
+
resource = parent_resource_name
|
|
214
|
+
final_path = "#{resource}/:id/#{path}"
|
|
215
|
+
final_to ||= "#{resource}##{path}"
|
|
216
|
+
elsif in_scope?(:collection) || in_scope?(:resource)
|
|
217
|
+
resource = parent_resource_name
|
|
218
|
+
final_path = "#{resource}/#{path}"
|
|
219
|
+
final_to ||= "#{resource}##{path}"
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
if final_to.nil?
|
|
223
|
+
raise ArgumentError, "Falta el destino 'to:' para la ruta #{method} '#{final_path}'. Usa la sintaxis 'controlador#accion'"
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
@routes << Route.new(method, final_path, to: final_to)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# --- LÓGICA DE SCOPES INTERNOS ---
|
|
230
|
+
|
|
231
|
+
# Abre un nuevo contexto de alcance temporal.
|
|
232
|
+
#
|
|
233
|
+
# @param scope [Hash] Información del nuevo alcance.
|
|
234
|
+
# @yield Bloque a ejecutar dentro de este alcance.
|
|
235
|
+
# @api private
|
|
236
|
+
def with_scope(scope)
|
|
237
|
+
@scopes << scope
|
|
238
|
+
yield
|
|
239
|
+
ensure
|
|
240
|
+
@scopes.pop
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# @return [Hash] El scope activo actualmente.
|
|
244
|
+
# @api private
|
|
245
|
+
def current_scope
|
|
246
|
+
@scopes.last || {}
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# @param type [Symbol] El tipo de scope a verificar.
|
|
250
|
+
# @return [Boolean] Si estamos dentro de un scope del tipo especificado.
|
|
251
|
+
# @api private
|
|
252
|
+
def in_scope?(type)
|
|
253
|
+
current_scope[:type] == type
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Busca hacia atrás en la pila de scopes para encontrar el nombre del recurso padre.
|
|
257
|
+
#
|
|
258
|
+
# @return [String, nil] El nombre del recurso o nil si no se encuentra.
|
|
259
|
+
# @api private
|
|
260
|
+
def parent_resource_name
|
|
261
|
+
@scopes.reverse_each do |scope|
|
|
262
|
+
return scope[:name] if scope[:type] == :resource
|
|
263
|
+
end
|
|
264
|
+
nil
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
data/lib/bug_bunny/version.rb
CHANGED
data/lib/bug_bunny.rb
CHANGED
|
@@ -5,6 +5,7 @@ require 'logger'
|
|
|
5
5
|
require_relative 'bug_bunny/version'
|
|
6
6
|
require_relative 'bug_bunny/exception'
|
|
7
7
|
require_relative 'bug_bunny/configuration'
|
|
8
|
+
require_relative 'bug_bunny/routing/route_set'
|
|
8
9
|
require_relative 'bug_bunny/middleware/base'
|
|
9
10
|
require_relative 'bug_bunny/middleware/stack'
|
|
10
11
|
require_relative 'bug_bunny/middleware/raise_error'
|
|
@@ -27,6 +28,13 @@ module BugBunny
|
|
|
27
28
|
|
|
28
29
|
# @return [Bunny::Session, nil] La conexión global (Singleton) usada por procesos Rails.
|
|
29
30
|
attr_accessor :global_connection
|
|
31
|
+
|
|
32
|
+
attr_writer :routes
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @return [BugBunny::Routing::RouteSet] El motor global de enrutamiento.
|
|
36
|
+
def self.routes
|
|
37
|
+
@routes ||= Routing::RouteSet.new
|
|
30
38
|
end
|
|
31
39
|
|
|
32
40
|
# Configura la librería BugBunny.
|
|
@@ -59,3 +59,20 @@ BugBunny.configure do |config|
|
|
|
59
59
|
# * Valor alto (10-50): Mayor throughput, pero riesgo de sobrecargar un solo worker lento.
|
|
60
60
|
config.channel_prefetch = 10
|
|
61
61
|
end
|
|
62
|
+
|
|
63
|
+
# ==========================================
|
|
64
|
+
# 🗺️ Enrutamiento Declarativo (Router)
|
|
65
|
+
# ==========================================
|
|
66
|
+
# Define cómo se mapean las rutas de los mensajes entrantes a tus controladores.
|
|
67
|
+
# Funciona de manera similar al routes.rb de Rails.
|
|
68
|
+
|
|
69
|
+
# BugBunny.routes.draw do
|
|
70
|
+
# # Macro para generar rutas CRUD estándar (index, show, create, update, destroy)
|
|
71
|
+
# resources :services
|
|
72
|
+
#
|
|
73
|
+
# # Rutas estáticas o custom
|
|
74
|
+
# get 'health_checks/up', to: 'health_checks#up'
|
|
75
|
+
#
|
|
76
|
+
# # Rutas con parámetros dinámicos (:id, :cluster_id, etc.)
|
|
77
|
+
# put 'nodes/:id/drain', to: 'nodes#drain'
|
|
78
|
+
# end
|
data/test/test_helper.rb
CHANGED
|
@@ -107,3 +107,14 @@ module IntegrationHelper
|
|
|
107
107
|
worker_thread&.kill
|
|
108
108
|
end
|
|
109
109
|
end
|
|
110
|
+
|
|
111
|
+
# test/test_helper.rb (Al final del archivo)
|
|
112
|
+
|
|
113
|
+
BugBunny.routes.draw do
|
|
114
|
+
# Rutas necesarias para infrastructure_test.rb
|
|
115
|
+
resources :ping
|
|
116
|
+
|
|
117
|
+
# Rutas necesarias para manual_client_test.rb
|
|
118
|
+
get 'echo', to: 'echo#index'
|
|
119
|
+
post 'test', to: 'test#index' # o el controlador que maneje 'test'
|
|
120
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bug_bunny
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 4.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- gabix
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-02
|
|
11
|
+
date: 2026-03-02 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bunny
|
|
@@ -247,6 +247,8 @@ files:
|
|
|
247
247
|
- lib/bug_bunny/railtie.rb
|
|
248
248
|
- lib/bug_bunny/request.rb
|
|
249
249
|
- lib/bug_bunny/resource.rb
|
|
250
|
+
- lib/bug_bunny/routing/route.rb
|
|
251
|
+
- lib/bug_bunny/routing/route_set.rb
|
|
250
252
|
- lib/bug_bunny/session.rb
|
|
251
253
|
- lib/bug_bunny/version.rb
|
|
252
254
|
- lib/generators/bug_bunny/install/install_generator.rb
|