bug_bunny 2.0.2 → 3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8fccb3590fd16133119a7e2cea4c05002964846ac5a35e0714925f2d87115a06
4
- data.tar.gz: 839ab64bb406eebee8e59f99c010c640a033dab15cd0f531e66f0fbc1e201c82
3
+ metadata.gz: 0e154bfecb70c1fc85408e87876976dfc8c64c01c2816c4f811a05e39e8432f9
4
+ data.tar.gz: 3b4e1508cd1a7f019fb638831c58117d00c46729b4f6bad4f8ecaf1b75914b47
5
5
  SHA512:
6
- metadata.gz: 4be8016e660aec70394814ffc85ecba71759aeb94df817abac412c201685ebee89c447fdd76b9904dfb5d2bc5f73e208ed0eed2d9bbc016a307c09978254c76e
7
- data.tar.gz: 741552a099ae0aadb5717cb46fc4ff24c77c9f31f7dae677131dc158f784a75bca1a739bb6cf65eba42bff84fba853d25a2df54ca0d5884a23f679fa72c679b0
6
+ metadata.gz: b43ec72552e6d17601665f7640aedd1e9c390463f6df5ca950b082e17316e387d0a10fa9e5ddfd3611d3f928a5d7d2af71367fa6c5409ba1f242402bd46843a1
7
+ data.tar.gz: cdd64f5401a653a0fe839ea2a3cf926e1af7de6e8498892bbd20876ea2ed808f97d4db1daa1a31f70c54533a00f73e834a547943d34f8a95543b1befeb5a9591
data/CHANGELOG.md CHANGED
@@ -1,3 +1,25 @@
1
1
  # Changelog
2
+
3
+ ## [3.0.0] - 2026-02-05
4
+
5
+ ### ⚠ Breaking Changes
6
+ * **Architecture Overhaul:** Complete rewrite implementing the "Active Record over AMQP" philosophy. The framework now simulates a RESTful architecture where messages contain "URLs" (via `type` header) that map to Controllers.
7
+ * **Resource Configuration:** Removed `routing_key_prefix` in favor of `resource_name`. By default, `resource_name` is now automatically pluralized (Rails-style) to determine routing keys and headers.
8
+ * **Schema-less Resources:** Removed strict `ActiveModel::Attributes` dependency. `BugBunny::Resource` no longer requires defining attributes manually. It now uses a dynamic storage (`@remote_attributes`) supporting arbitrary keys (including PascalCase for Docker APIs) via `method_missing`.
9
+
10
+ ### 🚀 New Features
11
+ * **Middleware Stack:** Implemented an "Onion Architecture" for the `Client` similar to Faraday. Added support for middleware chains to intercept requests/responses.
12
+ * `Middleware::JsonResponse`: Automatically parses JSON bodies and provides `IndifferentAccess`.
13
+ * `Middleware::RaiseError`: Maps AMQP/HTTP status codes to Ruby exceptions (e.g., 404 to `BugBunny::NotFound`).
14
+ * **REST-over-AMQP Routing:** The `Consumer` now parses the `type` header as a URL (e.g., `users/show/12?active=true`) to dispatch actions to specific Controllers.
15
+ * **Direct Reply-To RPC:** Optimized RPC calls to use RabbitMQ's native `amq.rabbitmq.reply-to` feature, eliminating the need for temporary reply queues and improving performance.
16
+ * **Config Inheritance:** `BugBunny::Resource` configurations (like `connection_pool`, `exchange`) are now inherited by child classes, simplifying setup for groups of models.
17
+
18
+ ### 🛠 Improvements
19
+ * **Connection Pooling:** Full integration with `connection_pool` to ensure thread safety in multi-threaded environments (Puma/Sidekiq).
20
+ * **Error Handling:** Unified exception hierarchy under `BugBunny::Error`, with specific classes for Client (4xx) and Server (5xx) errors.
21
+ * **Rails Integration:** Added `Railtie` with hooks for Puma and Spring to safely handle connection forks.
22
+ * **Documentation:** Added comprehensive YARD documentation for all core classes.
23
+
2
24
  ## Version 0.1.0
3
- Migration bunny logic from utils
25
+ * Migration bunny logic from utils
data/README.md CHANGED
@@ -1,183 +1,273 @@
1
- # BugBunny
1
+ # 🐰 BugBunny
2
2
 
3
- ## Configuration
3
+ **BugBunny** es un framework RPC para Ruby on Rails sobre **RabbitMQ**.
4
+
5
+ Su filosofía es **"Active Record over AMQP"**. Abstrae la complejidad de colas y exchanges transformando patrones de mensajería en una arquitectura **RESTful simulada**, donde los mensajes contienen "URLs" y "Query Params" que son enrutados automáticamente a controladores.
6
+
7
+ ---
8
+
9
+ ## 📦 Instalación
10
+
11
+ Agrega la gema a tu `Gemfile`:
4
12
 
5
13
  ```ruby
6
- config/initializers/bug_bunny.rb
7
- BugBunny.configure do |config|
8
- config.host = 'Host'
9
- config.username = 'Username'
10
- config.password = 'Password'
11
- config.vhost = '/'
12
- config.logger = Rails.logger
13
- config.automatically_recover = false
14
- config.network_recovery_interval = 5
15
- config.connection_timeout = 10
16
- config.read_timeout = 30
17
- config.write_timeout = 30
18
- config.heartbeat = 15
19
- config.continuation_timeout = 15_000
20
- end
14
+ gem 'bug_bunny'
21
15
  ```
22
16
 
23
- ## Publish
24
-
25
- ### Rutas
17
+ Ejecuta el bundle:
26
18
 
19
+ ```bash
20
+ bundle install
27
21
  ```
28
- # config/rabbit_rest.yml
29
- default: &default
30
- healt_check:
31
- up: 'healt_check/up'
32
- manager:
33
- services:
34
- index: 'services/index'
35
- create: 'services/create'
36
- show: 'services/%<id>s/show'
37
- update: 'services/%<id>s/update'
38
- destroy: 'services/%<id>s/destroy'
39
- swarm:
40
- info: 'swarm/info'
41
- version: 'swarm/version'
42
- swarm: 'swarm/swarm'
43
- tasks:
44
- index: 'tasks/index'
45
-
46
- development:
47
- <<: *default
48
-
49
- test:
50
- <<: *default
51
-
52
- production:
53
- <<: *default
54
22
 
23
+ Corre el instalador para generar la configuración:
24
+
25
+ ```bash
26
+ rails g bug_bunny:install
55
27
  ```
56
28
 
57
- ### Configuration
29
+ ---
30
+
31
+ ## ⚙️ Configuración
32
+
33
+ Configura tus credenciales y el Pool de conexiones en el inicializador.
58
34
 
59
35
  ```ruby
60
36
  # config/initializers/bug_bunny.rb
61
- BUG_BUNNY_ENDPOINTS = Rails.application.config_for(:rabbit_rest)
62
37
 
63
- BUNNY_POOL = ConnectionPool.new(size: RABBIT_MAX_THREADS) do
64
- BugBunny::Rabbit.create_connection(host: RABBIT_HOST, username: RABBIT_USER, password: RABBIT_PASS, vhost: RABBIT_VIRTUAL_HOST)
38
+ BugBunny.configure do |config|
39
+ config.host = ENV.fetch('RABBITMQ_HOST', 'localhost')
40
+ config.username = ENV.fetch('RABBITMQ_USER', 'guest')
41
+ config.password = ENV.fetch('RABBITMQ_PASS', 'guest')
42
+ config.vhost = ENV.fetch('RABBITMQ_VHOST', '/')
43
+
44
+ # Timeouts y Recuperación
45
+ config.rpc_timeout = 10 # Segundos a esperar respuesta síncrona
46
+ config.network_recovery_interval = 5
65
47
  end
48
+
49
+ # Definimos el Pool Global (Vital para Puma/Sidekiq)
50
+ BUG_BUNNY_POOL = ConnectionPool.new(size: ENV.fetch('RAILS_MAX_THREADS', 5).to_i, timeout: 5) do
51
+ BugBunny.create_connection
52
+ end
53
+
54
+ # Inyectamos el pool por defecto a los recursos
55
+ BugBunny::Resource.connection_pool = BUG_BUNNY_POOL
66
56
  ```
67
57
 
68
- ### Publisher
58
+ ---
69
59
 
70
- Creamos cualquier clase que herede de `BugBunny::Publisher`, luego definimos metodos de clase y dentro de cada una de ella su implementacion
60
+ ## 🚀 Modo Resource (ORM / Active Record)
71
61
 
72
- 1. Mensajes sincronicos
62
+ Define modelos que actúan como proxis de recursos remotos. BugBunny separa la **Lógica de Transporte** (RabbitMQ) de la **Lógica de Aplicación** (Controladores).
73
63
 
64
+ ### Escenario A: Routing Dinámico (Topic / Estándar)
65
+ Ideal cuando quieres enrutar por acción. La Routing Key se genera automáticamente usando `resource_name.action`.
66
+
67
+ ```ruby
68
+ class RemoteUser < BugBunny::Resource
69
+ # --- Configuración ---
70
+ self.exchange = 'app.topic'
71
+ self.exchange_type = 'topic'
72
+
73
+ # Define el nombre lógico del recurso.
74
+ # 1. Routing Key: 'users.create', 'users.show.12'
75
+ # 2. Header Type: 'users/create', 'users/show/12'
76
+ self.resource_name = 'users'
77
+
78
+ # No necesitas definir atributos, BugBunny soporta atributos dinámicos (Schema-less)
79
+ end
74
80
  ```
75
- class Rabbit::Publisher::Manager < BugBunny::Publisher
76
- ROUTING_KEY = :manager
77
- ROUTES = BUG_BUNNY_ENDPOINTS[:manager][:swarm]
78
81
 
79
- def self.info(exchange:, message: nil)
80
- obj = new(pool: NEW_BUNNY_POOL, exchange_name: exchange, action: self::ROUTES[:info], message: message)
81
- obj.publish_and_consume!
82
- end
82
+ ### Escenario B: Routing Estático (Direct / Cola Dedicada)
83
+ Ideal cuando quieres enviar todo a una cola específica (ej: un Manager), independientemente de la acción.
83
84
 
84
- def self.version(exchange:, message: nil)
85
- obj = new(pool: NEW_BUNNY_POOL, exchange_name: exchange, action: self::ROUTES[:version], message: message)
86
- obj.publish_and_consume!
87
- end
85
+ ```ruby
86
+ class BoxManager < BugBunny::Resource
87
+ # --- Configuración ---
88
+ self.exchange = 'warehouse.direct'
89
+ self.exchange_type = 'direct'
90
+
91
+ # FORZAMOS LA ROUTING KEY.
92
+ # Todo viaja con esta key, sin importar la acción.
93
+ self.routing_key = 'manager_queue'
94
+
95
+ # Define el nombre lógico para el Controlador.
96
+ # Header Type: 'box_manager/create', 'box_manager/show/12'
97
+ self.resource_name = 'box_manager'
88
98
  end
89
99
  ```
90
100
 
91
- 2. Mensajes Asincronicos
101
+ ### Consumiendo el Servicio (CRUD)
102
+
103
+ La API simula ActiveRecord. Por debajo, construye una "URL" en el header `type` para que el consumidor sepa qué hacer.
92
104
 
105
+ ```ruby
106
+ # --- READ (Colección con Filtros) ---
107
+ # Header Type: "users/index?active=true" (Query Params)
108
+ # Routing Key: "users.index" (Dinámico) o "manager_queue" (Estático)
109
+ users = RemoteUser.where(active: true)
110
+
111
+ # --- READ (Singular) ---
112
+ # Header Type: "users/show/123" (ID en Path)
113
+ # Routing Key: "users.show.123" (Dinámico) o "manager_queue" (Estático)
114
+ user = RemoteUser.find(123)
115
+ puts user.name # Acceso dinámico a atributos
116
+
117
+ # --- CREATE ---
118
+ # Header Type: "users/create"
119
+ user = RemoteUser.create(email: "test@test.com")
120
+
121
+ # --- UPDATE ---
122
+ # Header Type: "users/update/123"
123
+ user.update(email: "edit@test.com")
124
+ # Dirty Tracking: Solo se envían los atributos modificados.
125
+
126
+ # --- DESTROY ---
127
+ # Header Type: "users/destroy/123"
128
+ user.destroy
93
129
  ```
94
- class Rabbit::Publisher::Manager < BugBunny::Publisher
95
- ROUTING_KEY = :manager
96
- ROUTES = BUG_BUNNY_ENDPOINTS[:manager][:swarm]
97
130
 
98
- def self.info(exchange:, message: nil)
99
- obj = new(pool: NEW_BUNNY_POOL, exchange_name: exchange, action: self::ROUTES[:info], message: message)
100
- obj.publish!
101
- end
131
+ ---
102
132
 
103
- def self.version(exchange:, message: nil)
104
- obj = new(pool: NEW_BUNNY_POOL, exchange_name: exchange, action: self::ROUTES[:version], message: message)
105
- obj.publish!
106
- end
133
+ ## 🔌 Modo Publisher (Cliente Manual)
134
+
135
+ Si no necesitas mapear un recurso o quieres enviar mensajes crudos ("Fire-and-Forget"), puedes usar `BugBunny::Client` directamente.
136
+
137
+ ### 1. Instanciar el Cliente
138
+
139
+ ```ruby
140
+ # Puedes inyectar middlewares personalizados aquí si lo deseas
141
+ client = BugBunny::Client.new(pool: BUG_BUNNY_POOL) do |conn|
142
+ conn.use BugBunny::Middleware::JsonResponse
107
143
  end
108
144
  ```
109
145
 
110
- 3. Attributes del objeto BugBunny::Publisher
111
-
112
- - content_type
113
- - content_encoding
114
- - correlation_id
115
- - reply_to
116
- - message_id
117
- - timestamp
118
- - priority
119
- - expiration
120
- - user_id
121
- - app_id
122
- - action
123
- - aguments
124
- - cluster_id
125
- - persistent
126
- - expiration
127
-
128
- ## Consumer
146
+ ### 2. Publicar Asíncronamente (Fire-and-Forget)
147
+ Envía el mensaje y retorna inmediatamente. Ideal para eventos o logs.
129
148
 
149
+ ```ruby
150
+ # publish(url_logica, opciones)
151
+ client.publish('notifications/alert',
152
+ exchange: 'events.topic',
153
+ exchange_type: 'topic',
154
+ routing_key: 'alerts.critical',
155
+ body: { message: 'CPU High', server: 'web-1' }
156
+ )
130
157
  ```
131
- class Rabbit::Controllers::Application < BugBunny::Controller
158
+
159
+ ### 3. Petición Síncrona (RPC)
160
+ Envía el mensaje y bloquea el hilo esperando la respuesta del consumidor.
161
+
162
+ ```ruby
163
+ begin
164
+ # request(url_logica, opciones)
165
+ response = client.request('math/calculate',
166
+ exchange: 'rpc.direct',
167
+ routing_key: 'calculator',
168
+ body: { a: 10, b: 20 },
169
+ timeout: 5 # Segundos de espera máxima
170
+ )
171
+
172
+ puts response['body'] # => { "result": 30 }
173
+
174
+ rescue BugBunny::RequestTimeout
175
+ puts "El servidor tardó demasiado."
132
176
  end
177
+ ```
178
+
179
+ ---
133
180
 
134
- class Rabbit::Controllers::Swarm < Rabbit::Controllers::Application
135
- def info
136
- render status: :ok, json: Api::Docker.info
181
+ ## 📡 Modo Servidor (El Worker)
182
+
183
+ BugBunny incluye un **Router Inteligente** que parsea el header `type` (la URL simulada), extrae parámetros y despacha al controlador.
184
+
185
+ ### 1. Definir Controladores
186
+
187
+ Crea tus controladores en `app/rabbit/controllers/`. Heredan de `BugBunny::Controller`.
188
+
189
+ ```ruby
190
+ # app/rabbit/controllers/users_controller.rb
191
+ class UsersController < BugBunny::Controller
192
+
193
+ # Acción para type: "users/index?active=true"
194
+ def index
195
+ # params fusiona Query Params y Body
196
+ users = User.where(active: params[:active])
197
+ render status: 200, json: users
137
198
  end
138
199
 
139
- def version
140
- render status: :ok, json: Api::Docker.version
200
+ # Acción para type: "users/show/12"
201
+ def show
202
+ # params[:id] se extrae automáticamente del Path de la URL
203
+ user = User.find_by(id: params[:id])
204
+
205
+ if user
206
+ render status: 200, json: user
207
+ else
208
+ render status: 404, json: { error: 'Not Found' }
209
+ end
141
210
  end
142
211
 
143
- def swarm
144
- render status: :ok, json: Api::Docker.swarm
212
+ # Acción para type: "users/create"
213
+ def create
214
+ user = User.new(params)
215
+ if user.save
216
+ render status: 201, json: user
217
+ else
218
+ # Estos errores se propagan como BugBunny::UnprocessableEntity
219
+ render status: 422, json: { errors: user.errors }
220
+ end
145
221
  end
146
222
  end
147
-
148
223
  ```
149
224
 
150
- ## Resource
151
- Solo para recursos que se adaptan al crud de rails estoy utilizando automaticamente la logica de los publicadores. Los atributos solo se ponen si son necesarios, si no la dejas vacia y actua igual que active resource.
225
+ ### 2. Ejecutar el Worker
152
226
 
227
+ ```bash
228
+ bundle exec rake bug_bunny:work
153
229
  ```
154
- class Manager::Application < BugBunny::Resource
155
- self.resource_path = 'rabbit/publisher/manager'
156
-
157
- attribute :id # 'ID'
158
- attribute :version # 'Version'
159
- attribute :created_at # 'CreatedAt'
160
- attribute :update_at # 'UpdatedAt'
161
- attribute :spec # 'Spec'
162
- end
163
230
 
164
- class Manager::Service < Manager::Application
165
- attribute :endpoint # 'Endpoint'
166
- end
231
+ ---
232
+
233
+ ## 🏗 Arquitectura REST-over-AMQP
234
+
235
+ BugBunny desacopla el transporte de la lógica usando headers.
236
+
237
+ | Concepto | REST (HTTP) | BugBunny (AMQP) | Configuración |
238
+ | :--- | :--- | :--- | :--- |
239
+ | **Endpoint** | URL Path (`/users/1`) | Header `type` (`users/show/1`) | `resource_name` |
240
+ | **Filtros** | Query String (`?active=true`) | Header `type` (`users/index?active=true`) | Automático (`where`) |
241
+ | **Destino Físico** | IP/Dominio | Routing Key (`users.create` o `manager`) | `routing_key` (Estático) o `resource_name` (Dinámico) |
242
+ | **Payload** | Body (JSON) | Body (JSON) | N/A |
243
+ | **Status** | HTTP Code (200, 404) | JSON Response `status` | N/A |
244
+
245
+ ---
246
+
247
+ ## 🛠 Middlewares
248
+
249
+ BugBunny usa una pila de middlewares para procesar respuestas.
167
250
 
251
+ ```ruby
252
+ # Configuración global en el Resource
253
+ BugBunny::Resource.client_middleware do |conn|
254
+ # 1. Lanza excepciones Ruby para errores 4xx/5xx
255
+ conn.use BugBunny::Middleware::RaiseError
256
+
257
+ # 2. Parsea JSON a HashWithIndifferentAccess
258
+ conn.use BugBunny::Middleware::JsonResponse
259
+ end
168
260
  ```
169
261
 
170
- ## Exceptions
171
- - Error General:
172
- - `BugBunny::Error` hereda de `::StandardError` (Captura cualquier error de la gema.)
173
- - Error de Publicadores:
174
- - `BugBunny::PublishError` hereda de `BugBunny::Error` (Para fallos de envío o conexión.)
175
- - Error de Respuestas:
176
- - `BugBunny::ResponseError::Base` hereda de `BugBunny::Error` (Captura todos los errores de respuesta).
177
- - Errores Específicos de Respuesta:
178
- - `BugBunny::ResponseError::BadRequest`
179
- - `BugBunny::ResponseError::NotFound`
180
- - `BugBunny::ResponseError::NotAcceptable`
181
- - `BugBunny::ResponseError::RequestTimeout`
182
- - `BugBunny::ResponseError::UnprocessableEntity`: En este el error viene el error details a lo rails.
183
- - `BugBunny::ResponseError::InternalServerError`
262
+ ### Excepciones
263
+
264
+ * `BugBunny::UnprocessableEntity` (422): Error de validación.
265
+ * `BugBunny::NotFound` (404): Recurso no encontrado.
266
+ * `BugBunny::RequestTimeout`: Timeout RPC.
267
+ * `BugBunny::CommunicationError`: Fallo de red RabbitMQ.
268
+
269
+ ---
270
+
271
+ ## 📄 Licencia
272
+
273
+ Código abierto bajo [MIT License](https://opensource.org/licenses/MIT).
data/bin_client.rb ADDED
@@ -0,0 +1,51 @@
1
+ # bin_client.rb
2
+ require_relative 'lib/bug_bunny'
3
+ $stdout.sync = true # <--- Agrega esto
4
+
5
+ # 1. Configuración
6
+ BugBunny.configure do |config|
7
+ config.host = 'localhost'
8
+ config.username = 'wisproMQ'
9
+ config.password = 'wisproMQ'
10
+ config.vhost = 'sync.devel'
11
+ config.logger = Logger.new(STDOUT)
12
+ config.rpc_timeout = 5
13
+ end
14
+
15
+ # 2. Pool
16
+ POOL = ConnectionPool.new(size: 2, timeout: 5) do
17
+ BugBunny.create_connection
18
+ end
19
+
20
+ # 3. Cliente
21
+ client = BugBunny.new(pool: POOL)
22
+
23
+ # --- PRUEBA 1: Publish ---
24
+ puts "\n[1] Enviando mensaje asíncrono (Publish)..."
25
+
26
+ # AGREGADO: exchange_type: 'topic'
27
+ client.publish('test/ping', exchange: 'test_exchange', exchange_type: 'topic', routing_key: 'test.ping') do |req|
28
+ req.body = { msg: 'Hola, soy invisible' }
29
+ end
30
+
31
+ puts " -> Enviado."
32
+ sleep 1
33
+
34
+ # --- PRUEBA 2: RPC ---
35
+ puts "\n[2] Enviando petición síncrona (Request)..."
36
+
37
+ begin
38
+ # AGREGADO: exchange_type: 'topic'
39
+ response = client.request('test/123/ping', exchange: 'test_exchange', exchange_type: 'topic', routing_key: 'test.ping') do |req|
40
+ req.body = { data: 'Importante' }
41
+ req.timeout = 3
42
+ req.headers['X-Source'] = 'Terminal'
43
+ end
44
+
45
+ puts " -> ✅ RESPUESTA RECIBIDA:"
46
+ puts " Status: #{response['status']}"
47
+ puts " Body: #{response['body']}"
48
+
49
+ rescue BugBunny::RequestTimeout
50
+ puts " -> ❌ Error: Timeout esperando respuesta."
51
+ end
data/bin_suite.rb ADDED
@@ -0,0 +1,77 @@
1
+ # bin_suite.rb
2
+ require_relative 'test_helper'
3
+ require_relative 'test_resource' # Cargamos la clase TestUser
4
+
5
+ # Cliente "Raw" para pruebas manuales
6
+ raw_client = BugBunny.new(pool: TEST_POOL)
7
+
8
+ def assert(condition, msg)
9
+ if condition
10
+ puts " ✅ PASS: #{msg}"
11
+ else
12
+ puts " ❌ FAIL: #{msg}"
13
+ end
14
+ end
15
+
16
+ puts "\n🔎 --- INICIANDO SUITE DE PRUEBAS BUG BUNNY ---"
17
+
18
+ # ---------------------------------------------------------
19
+ # TEST 1: RPC Manual (Raw Client)
20
+ # ---------------------------------------------------------
21
+ puts "\n[1] Probando RPC Manual (Client#request)..."
22
+ begin
23
+ # Notar la routing key: test_user.ping
24
+ response = raw_client.request('test_user/ping', exchange: 'test_exchange', exchange_type: 'topic', routing_key: 'test_user.ping')
25
+ assert(response['body']['message'] == 'Pong!', "Respuesta RPC recibida correctamente")
26
+ rescue => e
27
+ assert(false, "Error RPC: #{e.message}")
28
+ end
29
+
30
+ # ---------------------------------------------------------
31
+ # TEST 2: Resource Finder (ORM)
32
+ # ---------------------------------------------------------
33
+ puts "\n[2] Probando BugBunny::Resource (Estilo Rails)..."
34
+
35
+ # YA NO NECESITAS with_scope
36
+ puts " -> Buscando usuario ID 123..."
37
+ user = TestUser.find(123)
38
+
39
+ assert(user.is_a?(TestUser), "El objeto retornado es un TestUser")
40
+ assert(user.name == "Gabriel", "El nombre cargó correctamente")
41
+ assert(user.persisted?, "El objeto figura como persistido")
42
+ # ---------------------------------------------------------
43
+ # TEST 3: Resource Create (ORM)
44
+ # ---------------------------------------------------------
45
+ puts "\n[3] Probando Resource Creation..."
46
+ puts " -> Creando usuario nuevo..."
47
+ new_user = TestUser.create(name: "Nuevo User", email: "new@test.com")
48
+ assert(new_user.persisted?, "El usuario se guardó y recibió ID")
49
+ assert(new_user.id.present?, "Tiene ID asignado por el worker (#{new_user.id})")
50
+
51
+ # ---------------------------------------------------------
52
+ # TEST 4: Validaciones Locales
53
+ # ---------------------------------------------------------
54
+ puts "\n[4] Probando Validaciones Locales..."
55
+ invalid_user = TestUser.new(email: "sin_nombre@test.com")
56
+ assert(invalid_user.valid? == false, "Usuario sin nombre es inválido")
57
+ assert(invalid_user.errors[:name].any?, "Tiene error en el campo :name")
58
+
59
+ puts "\n🏁 SUITE FINALIZADA"
60
+
61
+ # ---------------------------------------------------------
62
+ # TEST 5: Probando Configuración Dinámica (.with)...
63
+ # ---------------------------------------------------------
64
+ puts "\n[5] Probando Configuración Dinámica (.with)..."
65
+
66
+ # Probamos cambiar el routing key prefix temporalmente
67
+ # El worker escucha 'test_user.*', así que si cambiamos a 'bad_prefix', debería fallar o no encontrar nada
68
+ begin
69
+ # Forzamos una routing key que no existe para ver si respeta el cambio
70
+ TestUser.with(routing_key: 'ruta.incorrecta').find(123)
71
+ rescue BugBunny::RequestTimeout
72
+ puts " ✅ PASS: El override funcionó (timeout esperado en ruta incorrecta)"
73
+ end
74
+
75
+ # Probamos que vuelve a la normalidad
76
+ user = TestUser.find(123)
77
+ assert(user.present?, " ✅ PASS: La configuración volvió a la normalidad")
data/bin_worker.rb ADDED
@@ -0,0 +1,20 @@
1
+ # bin_worker.rb
2
+ require_relative 'test_helper'
3
+ require_relative 'test_controller'
4
+
5
+ puts "🐰 WORKER INICIADO (Exchange: Topic)..."
6
+
7
+ # Creamos la conexión (o usamos una del pool si quisieras)
8
+ connection = BugBunny.create_connection
9
+
10
+ # Usamos el método de clase directo.
11
+ # Al no pasar 'block: false', esto bloqueará la ejecución aquí mismo eternamente.
12
+ BugBunny::Consumer.subscribe(
13
+ connection: connection,
14
+ queue_name: 'test_users_queue',
15
+ exchange_name: 'test_exchange',
16
+ exchange_type: 'topic',
17
+ routing_key: 'test_user.#'
18
+ )
19
+
20
+ # ¡Ya no necesitas el loop! El subscribe mantiene vivo el proceso.
@@ -0,0 +1,27 @@
1
+ # config/initializers/bug_bunny.rb
2
+
3
+ # 1. Configuración Global de Conexión (Credenciales)
4
+ BugBunny.configure do |config|
5
+ config.host = ENV.fetch('RABBITMQ_HOST', 'localhost')
6
+ config.vhost = ENV.fetch('RABBITMQ_VHOST', '/')
7
+ config.username = ENV.fetch('RABBITMQ_USER', 'guest')
8
+ config.password = ENV.fetch('RABBITMQ_PASS', 'guest')
9
+ config.logger = Rails.logger
10
+ end
11
+
12
+ # 2. Crear el Pool de Conexiones (Vital para Puma/Sidekiq)
13
+ # Usamos una constante global o un singleton para mantener el pool vivo
14
+ BUG_BUNNY_POOL = ConnectionPool.new(size: ENV.fetch('RAILS_MAX_THREADS', 5).to_i, timeout: 5) do
15
+ BugBunny.create_connection
16
+ end
17
+
18
+ # 3. Inyectar el Pool en los Recursos
19
+ # Así todos tus modelos (User, Invoice, etc.) usan este pool por defecto
20
+ BugBunny::Resource.connection_pool = BUG_BUNNY_POOL
21
+
22
+ # 4. (Opcional) Configurar Middlewares Globales
23
+ # Si en el futuro quieres que BugBunny::Resource use middlewares (logs, tracing),
24
+ # podrías inyectar un cliente pre-configurado en lugar del pool directo.
25
+ # Por ahora, con el pool es suficiente para la v1.0.
26
+
27
+ puts "🐰 BugBunny inicializado con Pool de #{BUG_BUNNY_POOL.size} conexiones."