bug_bunny 2.0.2 → 3.0.1

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: 730ae48562650742b536036de0db8b811efa48855790ef08bffc15713867654e
4
+ data.tar.gz: c4d16c6b17307831dd9cfc3be7ef2f78ecc52254034ded5187b2a19f00490aa3
5
5
  SHA512:
6
- metadata.gz: 4be8016e660aec70394814ffc85ecba71759aeb94df817abac412c201685ebee89c447fdd76b9904dfb5d2bc5f73e208ed0eed2d9bbc016a307c09978254c76e
7
- data.tar.gz: 741552a099ae0aadb5717cb46fc4ff24c77c9f31f7dae677131dc158f784a75bca1a739bb6cf65eba42bff84fba853d25a2df54ca0d5884a23f679fa72c679b0
6
+ metadata.gz: 5551a1bff89d318faf4155dead6e042ac991b57a30459efa2bfcad9029191b12391e6e058e275dda39bdaa4ed7cd2413e46ececa048905836009e4294b8d85e2
7
+ data.tar.gz: dd6af676b128f77d01c885d1107b773080d3ebd80b0a49477f3f6d4db511f91acdf8d2775281fa4cae529e17161cf7180caf41c983aece183112f6ab93fedf69
data/CHANGELOG.md CHANGED
@@ -1,3 +1,40 @@
1
1
  # Changelog
2
+
3
+ ## [3.0.1] - 2026-02-10
4
+
5
+ ### 🚀 Features: RESTful Architecture
6
+ * **HTTP Verbs over AMQP:** Implemented support for semantic HTTP verbs (`GET`, `POST`, `PUT`, `DELETE`) within AMQP headers (`x-http-method`). This enables a true RESTful design over RabbitMQ.
7
+ * **Smart Router:** The `BugBunny::Consumer` now behaves like a Rails Router. It automatically infers the controller action based on the combination of the **Verb** and the **URL Path** (e.g., `GET users/1` dispatches to `show`, `POST users` to `create`).
8
+ * **Resource CRUD Mapping:** `BugBunny::Resource` now maps Ruby operations to their specific REST verbs:
9
+ * `create` -> `POST`
10
+ * `update` -> `PUT`
11
+ * `destroy` -> `DELETE`
12
+ * `find/where` -> `GET`.
13
+
14
+ ### 🛠 Improvements
15
+ * **Client API:** Updated `BugBunny::Client#request` and `#publish` to accept a `method:` argument (e.g., `client.request('users', method: :post)`), giving developers full control over the request semantics without changing the method signature.
16
+ * **Request Metadata:** `BugBunny::Request` now handles the `method` attribute and ensures it is properly injected into the AMQP headers for the consumer to read.
17
+
18
+ ## [3.0.0] - 2026-02-05
19
+
20
+ ### ⚠ Breaking Changes
21
+ * **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.
22
+ * **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.
23
+ * **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`.
24
+
25
+ ### 🚀 New Features
26
+ * **Middleware Stack:** Implemented an "Onion Architecture" for the `Client` similar to Faraday. Added support for middleware chains to intercept requests/responses.
27
+ * `Middleware::JsonResponse`: Automatically parses JSON bodies and provides `IndifferentAccess`.
28
+ * `Middleware::RaiseError`: Maps AMQP/HTTP status codes to Ruby exceptions (e.g., 404 to `BugBunny::NotFound`).
29
+ * **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.
30
+ * **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.
31
+ * **Config Inheritance:** `BugBunny::Resource` configurations (like `connection_pool`, `exchange`) are now inherited by child classes, simplifying setup for groups of models.
32
+
33
+ ### 🛠 Improvements
34
+ * **Connection Pooling:** Full integration with `connection_pool` to ensure thread safety in multi-threaded environments (Puma/Sidekiq).
35
+ * **Error Handling:** Unified exception hierarchy under `BugBunny::Error`, with specific classes for Client (4xx) and Server (5xx) errors.
36
+ * **Rails Integration:** Added `Railtie` with hooks for Puma and Spring to safely handle connection forks.
37
+ * **Documentation:** Added comprehensive YARD documentation for all core classes.
38
+
2
39
  ## Version 0.1.0
3
- Migration bunny logic from utils
40
+ * Migration bunny logic from utils
data/README.md CHANGED
@@ -1,183 +1,315 @@
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**.
6
+
7
+ A diferencia de otros clientes de RabbitMQ, BugBunny viaja con **Verbos HTTP** (`GET`, `POST`, `PUT`, `DELETE`) inyectados en los headers AMQP. Esto permite construir una API semántica donde un **Router Inteligente** despacha los mensajes a controladores Rails estándar.
8
+
9
+ ---
10
+
11
+ ## 📦 Instalación
12
+
13
+ Agrega la gema a tu `Gemfile`:
14
+
15
+ ```ruby
16
+ gem 'bug_bunny'
17
+ ```
18
+
19
+ Ejecuta el bundle:
20
+
21
+ ```bash
22
+ bundle install
23
+ ```
24
+
25
+ Corre el instalador para generar la configuración inicial:
26
+
27
+ ```bash
28
+ rails g bug_bunny:install
29
+ ```
30
+
31
+ ---
32
+
33
+ ## ⚙️ Configuración
34
+
35
+ Configura tus credenciales y el Pool de conexiones en el inicializador `config/initializers/bug_bunny.rb`.
4
36
 
5
37
  ```ruby
6
- config/initializers/bug_bunny.rb
7
38
  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
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
14
46
  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
47
  end
48
+
49
+ # ⚠️ CRÍTICO: Definimos el Pool Global
50
+ # Es vital usar ConnectionPool para garantizar la seguridad en entornos
51
+ # multi-hilo como Puma o Sidekiq.
52
+ BUG_BUNNY_POOL = ConnectionPool.new(size: ENV.fetch('RAILS_MAX_THREADS', 5).to_i, timeout: 5) do
53
+ BugBunny.create_connection
54
+ end
55
+
56
+ # Inyectamos el pool por defecto a los recursos
57
+ BugBunny::Resource.connection_pool = BUG_BUNNY_POOL
21
58
  ```
22
59
 
23
- ## Publish
60
+ ---
24
61
 
25
- ### Rutas
62
+ ## 🚀 Modo Resource (ORM / Active Record)
26
63
 
27
- ```
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
64
+ Define modelos que actúan como proxies de recursos remotos. BugBunny separa la **Lógica de Transporte** (RabbitMQ) de la **Lógica de Aplicación** (Controladores).
65
+
66
+ ### Definición del Modelo
54
67
 
68
+ ```ruby
69
+ class RemoteUser < BugBunny::Resource
70
+ # 1. Configuración de Transporte
71
+ self.exchange = 'app.topic'
72
+ self.exchange_type = 'topic'
73
+
74
+ # 2. Configuración Lógica (Routing)
75
+ # Define el nombre del recurso. Se usa para:
76
+ # - Routing Key automática: 'users' (Topic)
77
+ # - URL Base: 'users'
78
+ self.resource_name = 'users'
79
+
80
+ # Nota: BugBunny es Schema-less. No necesitas definir atributos.
81
+ # Soporta acceso dinámico: user.Name, user.email, etc.
82
+ end
55
83
  ```
56
84
 
57
- ### Configuration
85
+ ### Consumiendo el Servicio (CRUD RESTful)
86
+
87
+ BugBunny traduce automáticamente las llamadas de Ruby a peticiones HTTP simuladas.
58
88
 
59
89
  ```ruby
60
- # config/initializers/bug_bunny.rb
61
- BUG_BUNNY_ENDPOINTS = Rails.application.config_for(:rabbit_rest)
90
+ # --- READ COLLECTION (Index) ---
91
+ # Envia: GET users?active=true
92
+ # Routing Key: "users"
93
+ users = RemoteUser.where(active: true)
94
+
95
+ # --- READ MEMBER (Show) ---
96
+ # Envia: GET users/123
97
+ # Routing Key: "users"
98
+ user = RemoteUser.find(123)
99
+ puts user.email
100
+
101
+ # --- CREATE ---
102
+ # Envia: POST users
103
+ # Routing Key: "users"
104
+ # Body: { "email": "test@test.com", "role": "admin" }
105
+ user = RemoteUser.create(email: "test@test.com", role: "admin")
106
+
107
+ # --- UPDATE ---
108
+ # Envia: PUT users/123
109
+ # Routing Key: "users"
110
+ user.update(email: "edit@test.com")
111
+ # Dirty Tracking: Solo se envían los campos modificados.
112
+
113
+ # --- DESTROY ---
114
+ # Envia: DELETE users/123
115
+ # Routing Key: "users"
116
+ user.destroy
117
+ ```
118
+
119
+ ### Estrategias de Routing
120
+
121
+ Tienes 3 formas de controlar la `routing_key` hacia donde se envían los mensajes:
122
+
123
+ | Nivel | Método | Descripción | Ejemplo Config |
124
+ | :--- | :--- | :--- | :--- |
125
+ | **1. Dinámico** | `resource_name` | (Por defecto) Usa el nombre del recurso. | `self.resource_name = 'users'` -> Key `users` |
126
+ | **2. Estático** | `routing_key` | Fuerza TODO a una sola cola. | `self.routing_key = 'cola_manager'` |
127
+ | **3. Temporal** | `.with(...)` | Override solo para esa petición. | `User.with(routing_key: 'urgent').create` |
62
128
 
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)
129
+ ---
130
+
131
+ ## 🔌 Modo Publisher (Cliente Manual)
132
+
133
+ Si no necesitas mapear un recurso o quieres enviar mensajes crudos, utiliza `BugBunny::Client`. Soporta semántica REST pasando el argumento `method:`.
134
+
135
+ ### 1. Instanciar el Cliente
136
+
137
+ ```ruby
138
+ client = BugBunny::Client.new(pool: BUG_BUNNY_POOL) do |conn|
139
+ # Puedes inyectar middlewares aquí
140
+ conn.use BugBunny::Middleware::JsonResponse
65
141
  end
66
142
  ```
67
143
 
68
- ### Publisher
144
+ ### 2. Request (RPC Síncrono)
69
145
 
70
- Creamos cualquier clase que herede de `BugBunny::Publisher`, luego definimos metodos de clase y dentro de cada una de ella su implementacion
146
+ Envía el mensaje, **bloquea el hilo** y espera la respuesta JSON. Ideal para obtener datos.
71
147
 
72
- 1. Mensajes sincronicos
148
+ ```ruby
149
+ # GET (Leer)
150
+ response = client.request('users/123', method: :get)
151
+ puts response['body']
73
152
 
74
- ```
75
- class Rabbit::Publisher::Manager < BugBunny::Publisher
76
- ROUTING_KEY = :manager
77
- ROUTES = BUG_BUNNY_ENDPOINTS[:manager][:swarm]
153
+ # POST (Crear / Ejecutar)
154
+ response = client.request('math/calc', method: :post, body: { a: 10, b: 20 })
78
155
 
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
156
+ # PUT (Actualizar)
157
+ client.request('users/123', method: :put, body: { active: true })
83
158
 
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
88
- end
159
+ # DELETE (Borrar)
160
+ client.request('users/123', method: :delete)
89
161
  ```
90
162
 
91
- 2. Mensajes Asincronicos
163
+ ### 3. Publish (Asíncrono / Fire-and-Forget)
164
+
165
+ Envía el mensaje y retorna inmediatamente. No espera respuesta. Por defecto usa `method: :post` si no se especifica.
92
166
 
167
+ ```ruby
168
+ # Enviar log o evento
169
+ client.publish('logs/error', method: :post, body: { msg: 'Disk full' })
93
170
  ```
94
- class Rabbit::Publisher::Manager < BugBunny::Publisher
95
- ROUTING_KEY = :manager
96
- ROUTES = BUG_BUNNY_ENDPOINTS[:manager][:swarm]
97
171
 
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
172
+ ### 4. Configuración Avanzada (Bloques)
102
173
 
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
174
+ Puedes usar un bloque para configurar opciones de bajo nivel de AMQP (prioridad, expiración, headers, app_id).
175
+
176
+ ```ruby
177
+ client.publish('jobs/process') do |req|
178
+ req.method = :post
179
+ req.body = { image_id: 99 }
180
+
181
+ # Metadatos AMQP
182
+ req.priority = 9 # Alta prioridad (0-9)
183
+ req.expiration = '5000' # TTL 5 segundos (ms)
184
+ req.app_id = 'web-frontend'
185
+ req.headers['X-Trace-Id'] = 'abc-123'
107
186
  end
108
187
  ```
109
188
 
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
189
+ ### 5. Referencia de Opciones
129
190
 
130
- ```
131
- class Rabbit::Controllers::Application < BugBunny::Controller
132
- end
191
+ Estas opciones pueden pasarse como argumentos (`client.request(key: val)`) o dentro del bloque (`req.key = val`).
192
+
193
+ | Opción / Atributo | Tipo | Descripción | Default |
194
+ | :--- | :--- | :--- | :--- |
195
+ | `body` | `Hash/String` | El contenido del mensaje. | `nil` |
196
+ | `method` | `Symbol` | Verbo HTTP (`:get`, `:post`, `:put`, `:delete`). | `:get` (en request) |
197
+ | `exchange` | `String` | Nombre del Exchange destino. | `''` (Default Ex) |
198
+ | `routing_key` | `String` | Clave de ruteo. Si falta, usa el `path`. | `path` |
199
+ | `headers` | `Hash` | Headers personalizados. | `{}` |
200
+ | `timeout` | `Integer` | (Solo RPC) Segundos máx de espera. | Config global |
201
+ | `app_id` | `String` | ID de la aplicación origen. | `nil` |
202
+ | `priority` | `Integer` | Prioridad del mensaje (0-9). | `0` |
203
+ | `expiration` | `String` | TTL del mensaje en ms. | `nil` |
133
204
 
134
- class Rabbit::Controllers::Swarm < Rabbit::Controllers::Application
135
- def info
136
- render status: :ok, json: Api::Docker.info
205
+ ---
206
+
207
+ ## 📡 Modo Servidor (El Worker)
208
+
209
+ BugBunny incluye un **Router Inteligente** que funciona igual que el `config/routes.rb` de Rails. Infiere la acción basándose en el **Verbo HTTP** y la estructura de la **URL**.
210
+
211
+ ### 1. Definir Controladores
212
+
213
+ Crea tus controladores en `app/rabbit/controllers/`. Heredan de `BugBunny::Controller`.
214
+
215
+ ```ruby
216
+ # app/rabbit/controllers/users_controller.rb
217
+ class UsersController < BugBunny::Controller
218
+
219
+ # GET users
220
+ def index
221
+ users = User.where(active: params[:active])
222
+ render status: 200, json: users
137
223
  end
138
224
 
139
- def version
140
- render status: :ok, json: Api::Docker.version
225
+ # GET users/123
226
+ def show
227
+ user = User.find(params[:id])
228
+ render status: 200, json: user
141
229
  end
142
230
 
143
- def swarm
144
- render status: :ok, json: Api::Docker.swarm
231
+ # POST users
232
+ def create
233
+ user = User.new(params)
234
+ if user.save
235
+ render status: 201, json: user
236
+ else
237
+ # Estos errores se propagan como BugBunny::UnprocessableEntity
238
+ render status: 422, json: { errors: user.errors }
239
+ end
240
+ end
241
+
242
+ # PUT users/123
243
+ def update
244
+ # ...
145
245
  end
146
- end
147
246
 
247
+ # DELETE users/123
248
+ def destroy
249
+ # ...
250
+ end
251
+ end
148
252
  ```
149
253
 
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.
254
+ ### 2. Tabla de Ruteo (Convención)
255
+
256
+ El Router despacha automáticamente según esta tabla:
257
+
258
+ | Header `x-http-method` | Header `type` (URL) | Controlador | Acción |
259
+ | :--- | :--- | :--- | :--- |
260
+ | `GET` | `users` | `UsersController` | `index` |
261
+ | `GET` | `users/12` | `UsersController` | `show` |
262
+ | `POST` | `users` | `UsersController` | `create` |
263
+ | `PUT` | `users/12` | `UsersController` | `update` |
264
+ | `DELETE` | `users/12` | `UsersController` | `destroy` |
265
+ | `POST` | `users/12/promote` | `UsersController` | `promote` (Custom) |
152
266
 
267
+ ### 3. Ejecutar el Worker
268
+
269
+ ```bash
270
+ bundle exec rake bug_bunny:work
153
271
  ```
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
272
 
164
- class Manager::Service < Manager::Application
165
- attribute :endpoint # 'Endpoint'
166
- end
273
+ ---
274
+
275
+ ## 🏗 Arquitectura REST-over-AMQP
276
+
277
+ BugBunny desacopla el transporte de la lógica usando headers AMQP estándar.
278
+
279
+ | Concepto | REST (HTTP) | BugBunny (AMQP) |
280
+ | :--- | :--- | :--- |
281
+ | **Recurso** | `POST /users` | Header `type`: `users` + Header `x-http-method`: `POST` |
282
+ | **Parametros** | Query String / Body | Header `type` (Query) + Body (Payload) |
283
+ | **Destino** | DNS / IP | Routing Key (ej: `users`) |
284
+ | **Status** | HTTP Code (200, 404) | JSON Response `status` |
285
+
286
+ ---
167
287
 
288
+ ## 🛠 Middlewares
289
+
290
+ BugBunny usa una pila de middlewares para procesar peticiones y respuestas, permitiendo logging, manejo de errores y transformación de datos.
291
+
292
+ ```ruby
293
+ # Configuración global en el Resource
294
+ BugBunny::Resource.client_middleware do |conn|
295
+ # 1. Lanza excepciones Ruby para errores 4xx/5xx
296
+ conn.use BugBunny::Middleware::RaiseError
297
+
298
+ # 2. Parsea JSON a HashWithIndifferentAccess
299
+ conn.use BugBunny::Middleware::JsonResponse
300
+ end
168
301
  ```
169
302
 
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`
303
+ ### Excepciones Soportadas
304
+
305
+ * `BugBunny::BadRequest` (400)
306
+ * `BugBunny::NotFound` (404)
307
+ * `BugBunny::RequestTimeout` (408)
308
+ * `BugBunny::UnprocessableEntity` (422) - Incluye errores de validación.
309
+ * `BugBunny::InternalServerError` (500)
310
+
311
+ ---
312
+
313
+ ## 📄 Licencia
314
+
315
+ 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."