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 +4 -4
- data/CHANGELOG.md +38 -1
- data/README.md +266 -134
- data/bin_client.rb +51 -0
- data/bin_suite.rb +77 -0
- data/bin_worker.rb +20 -0
- data/initializer_example.rb +27 -0
- data/lib/bug_bunny/client.rb +102 -0
- data/lib/bug_bunny/config.rb +74 -3
- data/lib/bug_bunny/consumer.rb +161 -0
- data/lib/bug_bunny/controller.rb +74 -49
- data/lib/bug_bunny/exception.rb +81 -14
- data/lib/bug_bunny/middleware/json_response.rb +74 -0
- data/lib/bug_bunny/middleware/raise_error.rb +71 -0
- data/lib/bug_bunny/middleware/stack.rb +50 -0
- data/lib/bug_bunny/producer.rb +130 -0
- data/lib/bug_bunny/rabbit.rb +70 -300
- data/lib/bug_bunny/railtie.rb +54 -0
- data/lib/bug_bunny/request.rb +88 -0
- data/lib/bug_bunny/resource.rb +227 -169
- data/lib/bug_bunny/session.rb +82 -0
- data/lib/bug_bunny/version.rb +1 -1
- data/lib/bug_bunny.rb +104 -16
- data/lib/generators/bug_bunny/install/install_generator.rb +48 -0
- data/lib/generators/bug_bunny/install/templates/initializer.rb +61 -0
- data/test_controller.rb +49 -0
- data/test_helper.rb +20 -0
- data/test_resource.rb +22 -0
- metadata +178 -4
- data/lib/bug_bunny/publisher.rb +0 -108
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 730ae48562650742b536036de0db8b811efa48855790ef08bffc15713867654e
|
|
4
|
+
data.tar.gz: c4d16c6b17307831dd9cfc3be7ef2f78ecc52254034ded5187b2a19f00490aa3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
|
9
|
-
config.username = '
|
|
10
|
-
config.password = '
|
|
11
|
-
config.vhost
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
60
|
+
---
|
|
24
61
|
|
|
25
|
-
|
|
62
|
+
## 🚀 Modo Resource (ORM / Active Record)
|
|
26
63
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
###
|
|
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
|
-
#
|
|
61
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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
|
-
###
|
|
144
|
+
### 2. Request (RPC Síncrono)
|
|
69
145
|
|
|
70
|
-
|
|
146
|
+
Envía el mensaje, **bloquea el hilo** y espera la respuesta JSON. Ideal para obtener datos.
|
|
71
147
|
|
|
72
|
-
|
|
148
|
+
```ruby
|
|
149
|
+
# GET (Leer)
|
|
150
|
+
response = client.request('users/123', method: :get)
|
|
151
|
+
puts response['body']
|
|
73
152
|
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
obj.publish_and_consume!
|
|
82
|
-
end
|
|
156
|
+
# PUT (Actualizar)
|
|
157
|
+
client.request('users/123', method: :put, body: { active: true })
|
|
83
158
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
obj.publish_and_consume!
|
|
87
|
-
end
|
|
88
|
-
end
|
|
159
|
+
# DELETE (Borrar)
|
|
160
|
+
client.request('users/123', method: :delete)
|
|
89
161
|
```
|
|
90
162
|
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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
|
-
|
|
132
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
140
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
|
|
151
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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."
|