bug_bunny 3.0.2 → 3.0.4
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 +15 -0
- data/bin_suite.rb +41 -12
- data/lib/bug_bunny/consumer.rb +95 -53
- data/lib/bug_bunny/middleware/json_response.rb +38 -49
- data/lib/bug_bunny/middleware/raise_error.rb +21 -43
- data/lib/bug_bunny/middleware.rb +44 -0
- data/lib/bug_bunny/resource.rb +21 -19
- data/lib/bug_bunny/version.rb +1 -1
- data/lib/bug_bunny.rb +1 -0
- data/test_resource.rb +10 -13
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4752202964e5f4382a685a356a31264db3ae51049c42a49d3e5a6602fda10ed1
|
|
4
|
+
data.tar.gz: 1cb2f414e1fb52560d9a4310fd0679c1a59f86dc072fce6dd385b17006ecc6a0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 74af2e635f79d182cfe0fc336a67bf5175ad02729e39b5ddb77fb6bfdde4e3e733f863c78e453160ca6e03ff381dc81f1169a1c9d7be39a116d971638f61d846
|
|
7
|
+
data.tar.gz: c7da63dcce1cc2f0b77faa253327ddb4eec88866328429e84383172e07be402ff9d9c2ca6a2fbe16e9c0f7762bd8119c17496caa37a4ad35d0be07867e8aa78c
|
data/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
|
+
## [3.0.4] - 2026-02-16
|
|
3
|
+
|
|
4
|
+
### ♻️ Refactoring & Architecture
|
|
5
|
+
* **Middleware Architecture Overhaul:** Refactored the internal middleware stack to follow the **Template Method** pattern (Faraday-style).
|
|
6
|
+
* **New Base Class:** Introduced `BugBunny::Middleware` to standardize the execution flow (`call`, `app.call`).
|
|
7
|
+
* **Lifecycle Hooks:** Middlewares can now simply implement `on_request(env)` and/or `on_complete(response)` methods, eliminating the need to manually manage the execution chain.
|
|
8
|
+
* **Core Middlewares:** Refactored `RaiseError` and `JsonResponse` to use this new pattern, resulting in cleaner and more maintainable code.
|
|
9
|
+
* This change is **fully backward compatible** and paves the way for future middlewares (Loggers, Tracing, Headers injection).
|
|
10
|
+
|
|
11
|
+
## [3.0.3] - 2026-02-13
|
|
12
|
+
|
|
13
|
+
### 🐛 Bug Fixes
|
|
14
|
+
* **Nested Query Serialization:** Fixed an issue where passing nested hashes to `Resource.where` (e.g., `where(q: { service: 'rabbit' })`) produced invalid URL strings (Ruby's `to_s` format) instead of standard HTTP query parameters.
|
|
15
|
+
* **Resource:** Now uses `Rack::Utils.build_nested_query` to generate correct URLs (e.g., `?q[service]=rabbit`).
|
|
16
|
+
* **Consumer:** Now uses `Rack::Utils.parse_nested_query` to correctly reconstruct nested hashes from the query string.
|
|
2
17
|
|
|
3
18
|
## [3.0.2] - 2026-02-12
|
|
4
19
|
|
data/bin_suite.rb
CHANGED
|
@@ -24,7 +24,7 @@ begin
|
|
|
24
24
|
response = raw_client.request('test_user/ping', exchange: 'test_exchange', exchange_type: 'topic', routing_key: 'test_user.ping')
|
|
25
25
|
assert(response['body']['message'] == 'Pong!', "Respuesta RPC recibida correctamente")
|
|
26
26
|
rescue => e
|
|
27
|
-
assert(false, "Error RPC: #{e.message}")
|
|
27
|
+
assert(false, "Error RPC: #{e.class} - #{e.message}")
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
# ---------------------------------------------------------
|
|
@@ -36,17 +36,26 @@ puts "\n[2] Probando BugBunny::Resource (Estilo Rails)..."
|
|
|
36
36
|
puts " -> Buscando usuario ID 123..."
|
|
37
37
|
user = TestUser.find(123)
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
assert(user.
|
|
41
|
-
assert(user.
|
|
39
|
+
if user
|
|
40
|
+
assert(user.is_a?(TestUser), "El objeto retornado es un TestUser")
|
|
41
|
+
assert(user.name == "Gabriel", "El nombre cargó correctamente")
|
|
42
|
+
assert(user.persisted?, "El objeto figura como persistido")
|
|
43
|
+
else
|
|
44
|
+
assert(false, "No se encontró el usuario (Check worker logs)")
|
|
45
|
+
end
|
|
46
|
+
|
|
42
47
|
# ---------------------------------------------------------
|
|
43
48
|
# TEST 3: Resource Create (ORM)
|
|
44
49
|
# ---------------------------------------------------------
|
|
45
50
|
puts "\n[3] Probando Resource Creation..."
|
|
46
51
|
puts " -> Creando usuario nuevo..."
|
|
47
52
|
new_user = TestUser.create(name: "Nuevo User", email: "new@test.com")
|
|
48
|
-
|
|
49
|
-
assert(new_user.
|
|
53
|
+
if new_user.persisted?
|
|
54
|
+
assert(new_user.persisted?, "El usuario se guardó y recibió ID")
|
|
55
|
+
assert(new_user.id.present?, "Tiene ID asignado por el worker (#{new_user.id})")
|
|
56
|
+
else
|
|
57
|
+
assert(false, "Fallo al crear usuario: #{new_user.errors.full_messages}")
|
|
58
|
+
end
|
|
50
59
|
|
|
51
60
|
# ---------------------------------------------------------
|
|
52
61
|
# TEST 4: Validaciones Locales
|
|
@@ -56,22 +65,42 @@ invalid_user = TestUser.new(email: "sin_nombre@test.com")
|
|
|
56
65
|
assert(invalid_user.valid? == false, "Usuario sin nombre es inválido")
|
|
57
66
|
assert(invalid_user.errors[:name].any?, "Tiene error en el campo :name")
|
|
58
67
|
|
|
59
|
-
puts "\n🏁 SUITE FINALIZADA"
|
|
60
|
-
|
|
61
68
|
# ---------------------------------------------------------
|
|
62
69
|
# TEST 5: Probando Configuración Dinámica (.with)...
|
|
63
70
|
# ---------------------------------------------------------
|
|
64
71
|
puts "\n[5] Probando Configuración Dinámica (.with)..."
|
|
65
72
|
|
|
66
73
|
# 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
74
|
begin
|
|
69
|
-
# Forzamos una routing key que no existe
|
|
75
|
+
# Forzamos una routing key que no existe
|
|
76
|
+
puts " -> Intentando ruta incorrecta (esperando timeout)..."
|
|
70
77
|
TestUser.with(routing_key: 'ruta.incorrecta').find(123)
|
|
71
|
-
|
|
72
|
-
|
|
78
|
+
assert(false, "Debería haber fallado por timeout")
|
|
79
|
+
rescue BugBunny::RequestTimeout, BugBunny::ClientError
|
|
80
|
+
# Nota: Dependiendo de tu config, puede dar Timeout o 501 si llega a un worker default
|
|
81
|
+
puts " ✅ PASS: El override funcionó (timeout o error esperado en ruta incorrecta)"
|
|
73
82
|
end
|
|
74
83
|
|
|
75
84
|
# Probamos que vuelve a la normalidad
|
|
76
85
|
user = TestUser.find(123)
|
|
77
86
|
assert(user.present?, " ✅ PASS: La configuración volvió a la normalidad")
|
|
87
|
+
|
|
88
|
+
# ---------------------------------------------------------
|
|
89
|
+
# TEST 6: Filtrado Complejo (Query String Nested - FIX Rack)
|
|
90
|
+
# ---------------------------------------------------------
|
|
91
|
+
puts "\n[6] Probando Resource.where con filtros anidados (Fix Rack)..."
|
|
92
|
+
|
|
93
|
+
begin
|
|
94
|
+
# Esto fallaba antes (generaba string feo en la URL: {:active=>true})
|
|
95
|
+
# Al usar Rack, esto genera: ?q[active]=true&q[roles][]=admin
|
|
96
|
+
# No necesitamos que el worker responda algo real, solo que el request SALGA sin explotar URI.
|
|
97
|
+
TestUser.where(q: { active: true, roles: ['admin'] })
|
|
98
|
+
puts " ✅ PASS: .where generó la query anidada correctamente sin errores de URI."
|
|
99
|
+
rescue URI::InvalidURIError => e
|
|
100
|
+
assert(false, "❌ FAIL: URI Inválida (El fix de Rack no funcionó): #{e.message}")
|
|
101
|
+
rescue => e
|
|
102
|
+
# Si falla por conexión o 404 está bien, lo importante es que no falle al serializar
|
|
103
|
+
puts " ✅ PASS: El request se envió correctamente (aunque el worker responda: #{e.class}). Serialización OK."
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
puts "\n🏁 SUITE FINALIZADA"
|
data/lib/bug_bunny/consumer.rb
CHANGED
|
@@ -4,41 +4,58 @@ require 'concurrent'
|
|
|
4
4
|
require 'json'
|
|
5
5
|
require 'uri'
|
|
6
6
|
require 'cgi'
|
|
7
|
+
require 'rack/utils' # Necesario para parse_nested_query
|
|
7
8
|
|
|
8
9
|
module BugBunny
|
|
9
|
-
# Consumidor de mensajes
|
|
10
|
+
# Consumidor de mensajes AMQP que actúa como un Router RESTful.
|
|
10
11
|
#
|
|
11
|
-
# Esta clase
|
|
12
|
-
#
|
|
13
|
-
#
|
|
12
|
+
# Esta clase es el corazón del procesamiento de mensajes en el lado del servidor/worker.
|
|
13
|
+
# Sus responsabilidades son:
|
|
14
|
+
# 1. Escuchar una cola específica.
|
|
15
|
+
# 2. Deserializar el mensaje y sus headers.
|
|
16
|
+
# 3. Enrutar el mensaje a un Controlador (`BugBunny::Controller`) basándose en el "path" y el verbo HTTP.
|
|
17
|
+
# 4. Gestionar el ciclo de respuesta RPC (Request-Response) para evitar timeouts en el cliente.
|
|
14
18
|
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
19
|
+
# @example Suscripción manual
|
|
20
|
+
# connection = BugBunny.create_connection
|
|
21
|
+
# BugBunny::Consumer.subscribe(
|
|
22
|
+
# connection: connection,
|
|
23
|
+
# queue_name: 'my_app_queue',
|
|
24
|
+
# exchange_name: 'my_exchange',
|
|
25
|
+
# routing_key: 'users.#'
|
|
26
|
+
# )
|
|
17
27
|
class Consumer
|
|
18
|
-
# @return [BugBunny::Session] La sesión de RabbitMQ
|
|
28
|
+
# @return [BugBunny::Session] La sesión wrapper de RabbitMQ que gestiona el canal.
|
|
19
29
|
attr_reader :session
|
|
20
30
|
|
|
21
31
|
# Método de conveniencia para instanciar y suscribir en un solo paso.
|
|
22
|
-
#
|
|
23
|
-
# @param
|
|
32
|
+
#
|
|
33
|
+
# @param connection [Bunny::Session] Una conexión TCP activa a RabbitMQ.
|
|
34
|
+
# @param args [Hash] Argumentos que se pasarán al método {#subscribe}.
|
|
35
|
+
# @return [BugBunny::Consumer] La instancia del consumidor creada.
|
|
24
36
|
def self.subscribe(connection:, **args)
|
|
25
37
|
new(connection).subscribe(**args)
|
|
26
38
|
end
|
|
27
39
|
|
|
28
|
-
# Inicializa
|
|
40
|
+
# Inicializa un nuevo consumidor.
|
|
41
|
+
#
|
|
29
42
|
# @param connection [Bunny::Session] Conexión nativa de Bunny.
|
|
30
43
|
def initialize(connection)
|
|
31
44
|
@session = BugBunny::Session.new(connection)
|
|
32
45
|
end
|
|
33
46
|
|
|
34
|
-
# Inicia la suscripción a la cola y el
|
|
47
|
+
# Inicia la suscripción a la cola y comienza el bucle de procesamiento.
|
|
48
|
+
#
|
|
49
|
+
# Declara el exchange y la cola (si no existen), realiza el "binding" y
|
|
50
|
+
# se queda escuchando mensajes entrantes.
|
|
35
51
|
#
|
|
36
52
|
# @param queue_name [String] Nombre de la cola a escuchar.
|
|
37
|
-
# @param exchange_name [String]
|
|
38
|
-
# @param routing_key [String]
|
|
39
|
-
# @param exchange_type [String] Tipo de exchange ('direct', 'topic',
|
|
40
|
-
# @param queue_opts [Hash] Opciones
|
|
41
|
-
# @param block [Boolean] Si es true
|
|
53
|
+
# @param exchange_name [String] Nombre del exchange al cual enlazar la cola.
|
|
54
|
+
# @param routing_key [String] Patrón de enrutamiento (ej: 'users.*').
|
|
55
|
+
# @param exchange_type [String] Tipo de exchange ('direct', 'topic', 'fanout').
|
|
56
|
+
# @param queue_opts [Hash] Opciones adicionales para la cola (durable, auto_delete).
|
|
57
|
+
# @param block [Boolean] Si es `true`, bloquea el hilo actual (loop infinito).
|
|
58
|
+
# @return [void]
|
|
42
59
|
def subscribe(queue_name:, exchange_name:, routing_key:, exchange_type: 'direct', queue_opts: {}, block: true)
|
|
43
60
|
x = session.exchange(name: exchange_name, type: exchange_type)
|
|
44
61
|
q = session.queue(queue_name, queue_opts)
|
|
@@ -58,26 +75,25 @@ module BugBunny
|
|
|
58
75
|
|
|
59
76
|
private
|
|
60
77
|
|
|
61
|
-
# Procesa un mensaje individual.
|
|
78
|
+
# Procesa un mensaje individual recibido de la cola.
|
|
62
79
|
#
|
|
63
|
-
#
|
|
64
|
-
# 2. Enruta al controlador/acción.
|
|
65
|
-
# 3. Envía la respuesta RPC si es necesario.
|
|
66
|
-
# 4. Maneja excepciones y envía errores formateados al cliente.
|
|
80
|
+
# Realiza la orquestación completa: Parsing -> Routing -> Ejecución -> Respuesta.
|
|
67
81
|
#
|
|
68
|
-
# @param delivery_info [Bunny::DeliveryInfo] Metadatos de entrega.
|
|
69
|
-
# @param properties [Bunny::MessageProperties] Headers y propiedades AMQP.
|
|
70
|
-
# @param body [String]
|
|
82
|
+
# @param delivery_info [Bunny::DeliveryInfo] Metadatos de entrega (tag, redelivered, etc).
|
|
83
|
+
# @param properties [Bunny::MessageProperties] Headers y propiedades AMQP (reply_to, correlation_id).
|
|
84
|
+
# @param body [String] El payload crudo del mensaje.
|
|
85
|
+
# @return [void]
|
|
71
86
|
def process_message(delivery_info, properties, body)
|
|
72
87
|
if properties.type.nil? || properties.type.empty?
|
|
73
|
-
BugBunny.configuration.logger.error("[Consumer] Missing 'type'.
|
|
88
|
+
BugBunny.configuration.logger.error("[Consumer] Missing 'type' header. Message rejected.")
|
|
74
89
|
session.channel.reject(delivery_info.delivery_tag, false)
|
|
75
90
|
return
|
|
76
91
|
end
|
|
77
92
|
|
|
93
|
+
# 1. Determinar Verbo HTTP (Default: GET)
|
|
78
94
|
http_method = properties.headers ? (properties.headers['x-http-method'] || 'GET') : 'GET'
|
|
79
95
|
|
|
80
|
-
# Inferencia de
|
|
96
|
+
# 2. Router: Inferencia de Controlador y Acción
|
|
81
97
|
route_info = router_dispatch(http_method, properties.type)
|
|
82
98
|
|
|
83
99
|
headers = {
|
|
@@ -92,55 +108,57 @@ module BugBunny
|
|
|
92
108
|
reply_to: properties.reply_to
|
|
93
109
|
}
|
|
94
110
|
|
|
95
|
-
# Instanciación
|
|
111
|
+
# 3. Instanciación Dinámica del Controlador
|
|
96
112
|
# Ej: "users" -> Rabbit::Controllers::UsersController
|
|
97
113
|
controller_class_name = "rabbit/controllers/#{route_info[:controller]}".camelize
|
|
98
114
|
controller_class = controller_class_name.constantize
|
|
99
115
|
|
|
100
|
-
# Ejecución del
|
|
116
|
+
# 4. Ejecución del Pipeline (Filtros -> Acción)
|
|
101
117
|
response_payload = controller_class.call(headers: headers, body: body)
|
|
102
118
|
|
|
103
|
-
# Respuesta RPC (
|
|
119
|
+
# 5. Respuesta RPC (Si se solicita respuesta)
|
|
104
120
|
if properties.reply_to
|
|
105
121
|
reply(response_payload, properties.reply_to, properties.correlation_id)
|
|
106
122
|
end
|
|
107
123
|
|
|
124
|
+
# 6. Acknowledge (Confirmación de procesado)
|
|
108
125
|
session.channel.ack(delivery_info.delivery_tag)
|
|
109
126
|
|
|
110
127
|
rescue NameError => e
|
|
111
|
-
#
|
|
128
|
+
# Error 501/404: El controlador o la acción no existen.
|
|
112
129
|
BugBunny.configuration.logger.error("[Consumer] Routing Error: #{e.message}")
|
|
113
|
-
|
|
114
|
-
# FIX CRÍTICO: Responder con error para evitar Timeout en el cliente
|
|
115
|
-
if properties.reply_to
|
|
116
|
-
error_payload = { status: 501, body: { error: "Routing Error", detail: e.message } }
|
|
117
|
-
reply(error_payload, properties.reply_to, properties.correlation_id)
|
|
118
|
-
end
|
|
119
|
-
|
|
130
|
+
handle_fatal_error(properties, 501, "Routing Error", e.message)
|
|
120
131
|
session.channel.reject(delivery_info.delivery_tag, false)
|
|
121
132
|
|
|
122
133
|
rescue StandardError => e
|
|
123
|
-
#
|
|
134
|
+
# Error 500: Crash interno de la aplicación.
|
|
124
135
|
BugBunny.configuration.logger.error("[Consumer] Execution Error: #{e.message}")
|
|
125
|
-
|
|
126
|
-
# FIX CRÍTICO: Responder con 500 para evitar Timeout
|
|
127
|
-
if properties.reply_to
|
|
128
|
-
error_payload = { status: 500, body: { error: "Internal Server Error", detail: e.message } }
|
|
129
|
-
reply(error_payload, properties.reply_to, properties.correlation_id)
|
|
130
|
-
end
|
|
131
|
-
|
|
136
|
+
handle_fatal_error(properties, 500, "Internal Server Error", e.message)
|
|
132
137
|
session.channel.reject(delivery_info.delivery_tag, false)
|
|
133
138
|
end
|
|
134
139
|
|
|
135
|
-
#
|
|
136
|
-
# Convierte Verbo + Path en Controlador + Acción + ID.
|
|
140
|
+
# Interpreta la URL y el verbo para decidir qué controlador ejecutar.
|
|
137
141
|
#
|
|
138
|
-
#
|
|
142
|
+
# Utiliza `Rack::Utils.parse_nested_query` para soportar parámetros anidados
|
|
143
|
+
# como `q[service]=rabbit`.
|
|
144
|
+
#
|
|
145
|
+
# @param method [String] Verbo HTTP (GET, POST, etc).
|
|
146
|
+
# @param path [String] URL virtual del recurso (ej: 'users/1?active=true').
|
|
147
|
+
# @return [Hash] Estructura con keys {:controller, :action, :id, :params}.
|
|
139
148
|
def router_dispatch(method, path)
|
|
149
|
+
# Usamos URI para separar path de query string
|
|
140
150
|
uri = URI.parse("http://dummy/#{path}")
|
|
141
151
|
segments = uri.path.split('/').reject(&:empty?)
|
|
142
|
-
query_params = uri.query ? CGI.parse(uri.query).transform_values(&:first) : {}
|
|
143
152
|
|
|
153
|
+
# --- FIX: Uso de Rack para soportar params anidados ---
|
|
154
|
+
query_params = uri.query ? Rack::Utils.parse_nested_query(uri.query) : {}
|
|
155
|
+
|
|
156
|
+
# Si estamos en Rails, convertimos a HashWithIndifferentAccess para comodidad
|
|
157
|
+
if defined?(ActiveSupport::HashWithIndifferentAccess)
|
|
158
|
+
query_params = query_params.with_indifferent_access
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Lógica de Ruteo Convencional
|
|
144
162
|
controller_name = segments[0]
|
|
145
163
|
id = segments[1]
|
|
146
164
|
|
|
@@ -152,18 +170,24 @@ module BugBunny
|
|
|
152
170
|
else id || 'index'
|
|
153
171
|
end
|
|
154
172
|
|
|
155
|
-
# Soporte para
|
|
173
|
+
# Soporte para rutas miembro custom (POST users/1/promote)
|
|
156
174
|
if segments.size >= 3
|
|
157
175
|
id = segments[1]
|
|
158
176
|
action = segments[2]
|
|
159
177
|
end
|
|
160
178
|
|
|
179
|
+
# Inyectamos el ID en los params si existe en la ruta
|
|
161
180
|
query_params['id'] = id if id
|
|
162
181
|
|
|
163
182
|
{ controller: controller_name, action: action, id: id, params: query_params }
|
|
164
183
|
end
|
|
165
184
|
|
|
166
|
-
# Envía
|
|
185
|
+
# Envía una respuesta al cliente RPC utilizando Direct Reply-to.
|
|
186
|
+
#
|
|
187
|
+
# @param payload [Hash] Cuerpo de la respuesta ({ status: ..., body: ... }).
|
|
188
|
+
# @param reply_to [String] Cola de respuesta (generalmente pseudo-cola amq.rabbitmq.reply-to).
|
|
189
|
+
# @param correlation_id [String] ID para correlacionar la respuesta con la petición original.
|
|
190
|
+
# @return [void]
|
|
167
191
|
def reply(payload, reply_to, correlation_id)
|
|
168
192
|
session.channel.default_exchange.publish(
|
|
169
193
|
payload.to_json,
|
|
@@ -173,11 +197,29 @@ module BugBunny
|
|
|
173
197
|
)
|
|
174
198
|
end
|
|
175
199
|
|
|
176
|
-
#
|
|
200
|
+
# Maneja errores fatales asegurando que el cliente reciba una respuesta.
|
|
201
|
+
# Evita que el cliente RPC se quede esperando hasta el timeout.
|
|
202
|
+
#
|
|
203
|
+
# @api private
|
|
204
|
+
def handle_fatal_error(properties, status, error_title, detail)
|
|
205
|
+
return unless properties.reply_to
|
|
206
|
+
|
|
207
|
+
error_payload = {
|
|
208
|
+
status: status,
|
|
209
|
+
body: { error: error_title, detail: detail }
|
|
210
|
+
}
|
|
211
|
+
reply(error_payload, properties.reply_to, properties.correlation_id)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Tarea de fondo (Heartbeat lógico) para verificar la salud del canal.
|
|
215
|
+
# Si la cola desaparece o la conexión se cierra, fuerza una reconexión.
|
|
216
|
+
#
|
|
217
|
+
# @param q_name [String] Nombre de la cola a monitorear.
|
|
177
218
|
def start_health_check(q_name)
|
|
178
|
-
Concurrent::TimerTask.new(execution_interval:
|
|
219
|
+
Concurrent::TimerTask.new(execution_interval: BugBunny.configuration.health_check_interval) do
|
|
179
220
|
session.channel.queue_declare(q_name, passive: true)
|
|
180
221
|
rescue StandardError
|
|
222
|
+
BugBunny.configuration.logger.warn("[Consumer] Queue check failed. Reconnecting session...")
|
|
181
223
|
session.close
|
|
182
224
|
end.execute
|
|
183
225
|
end
|
|
@@ -1,73 +1,62 @@
|
|
|
1
1
|
# lib/bug_bunny/middleware/json_response.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
2
4
|
require 'json'
|
|
5
|
+
require_relative '../middleware'
|
|
3
6
|
|
|
4
7
|
module BugBunny
|
|
5
8
|
module Middleware
|
|
6
9
|
# Middleware encargado de parsear automáticamente el cuerpo de la respuesta.
|
|
7
10
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# **Integración con Rails:**
|
|
12
|
-
# Si `ActiveSupport` está cargado en el entorno, convierte los Hashes resultantes
|
|
13
|
-
# a `HashWithIndifferentAccess`. Esto permite a los desarrolladores acceder a las claves
|
|
14
|
-
# usando símbolos o strings indistintamente (ej: `body[:id]` o `body['id']`),
|
|
15
|
-
# comportamiento estándar en Rails.
|
|
11
|
+
# Convierte strings JSON en Hashes de Ruby. Si está disponible ActiveSupport,
|
|
12
|
+
# aplica HashWithIndifferentAccess.
|
|
16
13
|
#
|
|
17
|
-
# @
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
# conn.use BugBunny::Middleware::RaiseError
|
|
21
|
-
# conn.use BugBunny::Middleware::JsonResponse
|
|
22
|
-
# end
|
|
23
|
-
class JsonResponse
|
|
24
|
-
# Inicializa el middleware.
|
|
25
|
-
#
|
|
26
|
-
# @param app [Object] El siguiente middleware o el productor final en el stack.
|
|
27
|
-
def initialize(app)
|
|
28
|
-
@app = app
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# Ejecuta el middleware.
|
|
14
|
+
# @see BugBunny::Middleware
|
|
15
|
+
class JsonResponse < BugBunny::Middleware
|
|
16
|
+
# Hook de ciclo de vida: Ejecutado después de recibir la respuesta.
|
|
32
17
|
#
|
|
33
|
-
#
|
|
34
|
-
# Una vez recibida la respuesta, procesa el `body` antes de devolverla hacia arriba en la cadena.
|
|
18
|
+
# Intercepta el body y lo reemplaza por su versión parseada.
|
|
35
19
|
#
|
|
36
|
-
# @param
|
|
37
|
-
# @return [
|
|
38
|
-
def
|
|
39
|
-
response = @app.call(env)
|
|
40
|
-
# Parseamos el body DESPUÉS de recibir la respuesta (Post-processing)
|
|
20
|
+
# @param response [Hash] La respuesta cruda.
|
|
21
|
+
# @return [void]
|
|
22
|
+
def on_complete(response)
|
|
41
23
|
response['body'] = parse_body(response['body'])
|
|
42
|
-
response
|
|
43
24
|
end
|
|
44
25
|
|
|
26
|
+
private
|
|
27
|
+
|
|
45
28
|
# Intenta convertir el cuerpo de la respuesta a una estructura Ruby nativa.
|
|
46
29
|
#
|
|
47
|
-
# @param body [String, Hash, Array, nil] El cuerpo original
|
|
48
|
-
# @return [Object] El cuerpo parseado
|
|
49
|
-
# @api private
|
|
30
|
+
# @param body [String, Hash, Array, nil] El cuerpo original.
|
|
31
|
+
# @return [Object] El cuerpo parseado o el original si falla.
|
|
50
32
|
def parse_body(body)
|
|
51
|
-
return nil if body.nil? || body.empty?
|
|
33
|
+
return nil if body.nil? || (body.respond_to?(:empty?) && body.empty?)
|
|
52
34
|
|
|
53
|
-
# Si ya es un objeto (ej:
|
|
54
|
-
parsed = body.is_a?(String) ?
|
|
35
|
+
# Si ya es un objeto (ej: mocks), lo dejamos pasar; si es String, parseamos.
|
|
36
|
+
parsed = body.is_a?(String) ? safe_json_parse(body) : body
|
|
55
37
|
|
|
56
38
|
# Rails Magic: Indifferent Access
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if parsed.is_a?(Array)
|
|
60
|
-
parsed.map! { |e| e.try(:with_indifferent_access) || e }
|
|
61
|
-
elsif parsed.is_a?(Hash)
|
|
62
|
-
parsed = parsed.with_indifferent_access
|
|
63
|
-
end
|
|
64
|
-
end
|
|
39
|
+
apply_indifferent_access(parsed)
|
|
40
|
+
end
|
|
65
41
|
|
|
66
|
-
|
|
42
|
+
# Parsea JSON de forma segura, retornando el original si falla.
|
|
43
|
+
def safe_json_parse(json_string)
|
|
44
|
+
JSON.parse(json_string)
|
|
67
45
|
rescue JSON::ParserError
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
46
|
+
json_string
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Aplica ActiveSupport::HashWithIndifferentAccess si es posible.
|
|
50
|
+
def apply_indifferent_access(data)
|
|
51
|
+
return data unless defined?(ActiveSupport)
|
|
52
|
+
|
|
53
|
+
if data.is_a?(Array)
|
|
54
|
+
data.map! { |e| e.try(:with_indifferent_access) || e }
|
|
55
|
+
elsif data.is_a?(Hash)
|
|
56
|
+
data.with_indifferent_access
|
|
57
|
+
else
|
|
58
|
+
data
|
|
59
|
+
end
|
|
71
60
|
end
|
|
72
61
|
end
|
|
73
62
|
end
|
|
@@ -1,61 +1,30 @@
|
|
|
1
1
|
# lib/bug_bunny/middleware/raise_error.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative '../middleware'
|
|
5
|
+
|
|
2
6
|
module BugBunny
|
|
3
7
|
module Middleware
|
|
4
8
|
# Middleware que inspecciona el status de la respuesta y lanza excepciones
|
|
5
9
|
# si se encuentran errores (4xx o 5xx).
|
|
6
10
|
#
|
|
7
|
-
#
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
# @note Orden de Middlewares:
|
|
11
|
-
# Se recomienda usar este middleware **antes** de `JsonResponse` si deseas que
|
|
12
|
-
# la excepción contenga el cuerpo ya parseado (Hash).
|
|
13
|
-
#
|
|
14
|
-
# @example Configuración recomendada
|
|
15
|
-
# client = BugBunny::Client.new(pool: POOL) do |conn|
|
|
16
|
-
# conn.use BugBunny::Middleware::RaiseError # 1. Verifica errores primero (al salir)
|
|
17
|
-
# conn.use BugBunny::Middleware::JsonResponse # 2. Parsea JSON
|
|
18
|
-
# end
|
|
19
|
-
class RaiseError
|
|
20
|
-
# Inicializa el middleware.
|
|
21
|
-
# @param app [Object] El siguiente middleware o la aplicación final.
|
|
22
|
-
def initialize(app)
|
|
23
|
-
@app = app
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# Ejecuta el middleware.
|
|
27
|
-
# Realiza la petición y, al retornar, verifica el estado de la respuesta.
|
|
11
|
+
# @see BugBunny::Middleware
|
|
12
|
+
class RaiseError < BugBunny::Middleware
|
|
13
|
+
# Hook de ciclo de vida: Ejecutado después de recibir la respuesta.
|
|
28
14
|
#
|
|
29
|
-
# @param env [BugBunny::Request] El objeto request actual.
|
|
30
|
-
# @return [Hash] La respuesta si el status es exitoso (2xx).
|
|
31
|
-
# @raise [BugBunny::ClientError] Si el status es 4xx.
|
|
32
|
-
# @raise [BugBunny::ServerError] Si el status es 5xx.
|
|
33
|
-
def call(env)
|
|
34
|
-
response = @app.call(env)
|
|
35
|
-
on_complete(response)
|
|
36
|
-
response
|
|
37
|
-
end
|
|
38
|
-
|
|
39
15
|
# Verifica el código de estado y lanza la excepción correspondiente.
|
|
40
16
|
#
|
|
41
|
-
# Mapeo de errores:
|
|
42
|
-
# * 400 -> {BugBunny::BadRequest}
|
|
43
|
-
# * 404 -> {BugBunny::NotFound}
|
|
44
|
-
# * 406 -> {BugBunny::NotAcceptable}
|
|
45
|
-
# * 408 -> {BugBunny::RequestTimeout}
|
|
46
|
-
# * 422 -> {BugBunny::UnprocessableEntity}
|
|
47
|
-
# * 500 -> {BugBunny::InternalServerError}
|
|
48
|
-
# * Otros 4xx -> {BugBunny::ClientError}
|
|
49
|
-
#
|
|
50
17
|
# @param response [Hash] El hash de respuesta conteniendo 'status' y 'body'.
|
|
18
|
+
# @raise [BugBunny::ClientError] Si el status es 4xx.
|
|
19
|
+
# @raise [BugBunny::ServerError] Si el status es 5xx.
|
|
51
20
|
# @return [void]
|
|
52
21
|
def on_complete(response)
|
|
53
22
|
status = response['status'].to_i
|
|
54
|
-
body = response['body']
|
|
23
|
+
body = response['body']
|
|
55
24
|
|
|
56
25
|
case status
|
|
57
26
|
when 200..299
|
|
58
|
-
# OK
|
|
27
|
+
nil # OK
|
|
59
28
|
when 400 then raise BugBunny::BadRequest, body
|
|
60
29
|
when 404 then raise BugBunny::NotFound
|
|
61
30
|
when 406 then raise BugBunny::NotAcceptable
|
|
@@ -63,9 +32,18 @@ module BugBunny
|
|
|
63
32
|
when 422 then raise BugBunny::UnprocessableEntity, body
|
|
64
33
|
when 500 then raise BugBunny::InternalServerError, body
|
|
65
34
|
else
|
|
66
|
-
|
|
35
|
+
handle_unknown_error(status)
|
|
67
36
|
end
|
|
68
37
|
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
# Maneja errores 4xx genéricos no mapeados explícitamente.
|
|
42
|
+
# @param status [Integer] El código de estado HTTP.
|
|
43
|
+
# @raise [BugBunny::ClientError] Siempre lanza esta excepción.
|
|
44
|
+
def handle_unknown_error(status)
|
|
45
|
+
raise BugBunny::ClientError, "Unknown error: #{status}" if status >= 400
|
|
46
|
+
end
|
|
69
47
|
end
|
|
70
48
|
end
|
|
71
49
|
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# lib/bug_bunny/middleware.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module BugBunny
|
|
5
|
+
# Clase base para todos los middlewares de BugBunny.
|
|
6
|
+
#
|
|
7
|
+
# Implementa el patrón "Template Method" para estandarizar el flujo de ejecución
|
|
8
|
+
# de la cadena de responsabilidades (Ida y Vuelta).
|
|
9
|
+
#
|
|
10
|
+
# Las subclases deben implementar:
|
|
11
|
+
# * {#on_request} para modificar la petición antes de enviarla.
|
|
12
|
+
# * {#on_complete} para modificar la respuesta después de recibirla.
|
|
13
|
+
#
|
|
14
|
+
# @abstract Subclase y anula {#on_request} o {#on_complete} para inyectar lógica.
|
|
15
|
+
class Middleware
|
|
16
|
+
# @return [Object] El siguiente middleware en la pila o el adaptador final.
|
|
17
|
+
attr_reader :app
|
|
18
|
+
|
|
19
|
+
# Inicializa el middleware.
|
|
20
|
+
#
|
|
21
|
+
# @param app [Object] El siguiente eslabón de la cadena.
|
|
22
|
+
def initialize(app)
|
|
23
|
+
@app = app
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Ejecuta el middleware orquestando los hooks de ciclo de vida.
|
|
27
|
+
#
|
|
28
|
+
# 1. Llama a {#on_request} (Ida).
|
|
29
|
+
# 2. Llama al siguiente eslabón (`@app.call`).
|
|
30
|
+
# 3. Llama a {#on_complete} (Vuelta).
|
|
31
|
+
#
|
|
32
|
+
# @param env [BugBunny::Request] El objeto request (entorno).
|
|
33
|
+
# @return [Hash] La respuesta final procesada.
|
|
34
|
+
def call(env)
|
|
35
|
+
on_request(env) if respond_to?(:on_request)
|
|
36
|
+
|
|
37
|
+
response = @app.call(env)
|
|
38
|
+
|
|
39
|
+
on_complete(response) if respond_to?(:on_complete)
|
|
40
|
+
|
|
41
|
+
response
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
data/lib/bug_bunny/resource.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
require 'active_model'
|
|
3
3
|
require 'active_support/core_ext/string/inflections'
|
|
4
4
|
require 'uri'
|
|
5
|
+
require 'rack/utils'
|
|
5
6
|
|
|
6
7
|
module BugBunny
|
|
7
8
|
# Clase base para modelos remotos que implementan **Active Record over AMQP (RESTful)**.
|
|
@@ -17,7 +18,7 @@ module BugBunny
|
|
|
17
18
|
# self.exchange = 'app.topic'
|
|
18
19
|
# self.resource_name = 'users'
|
|
19
20
|
# # Opcional: Personalizar la clave raíz del JSON
|
|
20
|
-
# self.param_key = 'user_data'
|
|
21
|
+
# self.param_key = 'user_data'
|
|
21
22
|
# end
|
|
22
23
|
#
|
|
23
24
|
# @example Uso con contexto temporal
|
|
@@ -34,16 +35,16 @@ module BugBunny
|
|
|
34
35
|
|
|
35
36
|
# @return [HashWithIndifferentAccess] Contenedor de los atributos remotos (JSON crudo).
|
|
36
37
|
attr_reader :remote_attributes
|
|
37
|
-
|
|
38
|
+
|
|
38
39
|
# @return [Boolean] Indica si el objeto ha sido guardado en el servicio remoto.
|
|
39
40
|
attr_accessor :persisted
|
|
40
41
|
|
|
41
42
|
# @return [String, nil] Routing Key capturada en el momento de la instanciación.
|
|
42
43
|
attr_accessor :routing_key
|
|
43
|
-
|
|
44
|
+
|
|
44
45
|
# @return [String, nil] Exchange capturado en el momento de la instanciación.
|
|
45
46
|
attr_accessor :exchange
|
|
46
|
-
|
|
47
|
+
|
|
47
48
|
# @return [String, nil] Tipo de Exchange capturado en el momento de la instanciación.
|
|
48
49
|
attr_accessor :exchange_type
|
|
49
50
|
|
|
@@ -76,21 +77,21 @@ module BugBunny
|
|
|
76
77
|
|
|
77
78
|
# @return [ConnectionPool] El pool de conexiones asignado.
|
|
78
79
|
def connection_pool; resolve_config(:pool, :@connection_pool); end
|
|
79
|
-
|
|
80
|
+
|
|
80
81
|
# @return [String] El exchange configurado.
|
|
81
82
|
# @raise [ArgumentError] Si no se ha definido un exchange.
|
|
82
83
|
def current_exchange; resolve_config(:exchange, :@exchange) || raise(ArgumentError, "Exchange not defined"); end
|
|
83
|
-
|
|
84
|
+
|
|
84
85
|
# @return [String] El tipo de exchange (default: direct).
|
|
85
86
|
def current_exchange_type; resolve_config(:exchange_type, :@exchange_type) || 'direct'; end
|
|
86
|
-
|
|
87
|
+
|
|
87
88
|
# @return [String] El nombre del recurso (ej: 'users'). Se infiere del nombre de la clase si no existe.
|
|
88
89
|
def resource_name
|
|
89
90
|
resolve_config(:resource_name, :@resource_name) || name.demodulize.underscore.pluralize
|
|
90
91
|
end
|
|
91
92
|
|
|
92
93
|
# Define la clave raíz para envolver el payload JSON (Wrapping).
|
|
93
|
-
#
|
|
94
|
+
#
|
|
94
95
|
# Por defecto utiliza `model_name.element`, lo que elimina los namespaces.
|
|
95
96
|
# Ej: `Manager::Service` -> `'service'`.
|
|
96
97
|
#
|
|
@@ -122,7 +123,7 @@ module BugBunny
|
|
|
122
123
|
def bug_bunny_client
|
|
123
124
|
pool = connection_pool
|
|
124
125
|
raise BugBunny::Error, "Connection pool missing for #{name}" unless pool
|
|
125
|
-
|
|
126
|
+
|
|
126
127
|
BugBunny::Client.new(pool: pool) do |conn|
|
|
127
128
|
resolve_middleware_stack.each { |block| block.call(conn) }
|
|
128
129
|
end
|
|
@@ -137,7 +138,7 @@ module BugBunny
|
|
|
137
138
|
keys = { exchange: "bb_#{object_id}_exchange", exchange_type: "bb_#{object_id}_exchange_type", pool: "bb_#{object_id}_pool", routing_key: "bb_#{object_id}_routing_key" }
|
|
138
139
|
old_values = {}
|
|
139
140
|
keys.each { |k, v| old_values[k] = Thread.current[v] }
|
|
140
|
-
|
|
141
|
+
|
|
141
142
|
# Seteamos valores temporales
|
|
142
143
|
Thread.current[keys[:exchange]] = exchange if exchange
|
|
143
144
|
Thread.current[keys[:exchange_type]] = exchange_type if exchange_type
|
|
@@ -150,7 +151,7 @@ module BugBunny
|
|
|
150
151
|
ScopeProxy.new(self, keys, old_values)
|
|
151
152
|
end
|
|
152
153
|
end
|
|
153
|
-
|
|
154
|
+
|
|
154
155
|
# Proxy para permitir encadenamiento: User.with(...).find(1)
|
|
155
156
|
class ScopeProxy < BasicObject
|
|
156
157
|
def initialize(target, keys, old_values); @target = target; @keys = keys; @old_values = old_values; end
|
|
@@ -179,13 +180,14 @@ module BugBunny
|
|
|
179
180
|
def where(filters = {})
|
|
180
181
|
rk = calculate_routing_key
|
|
181
182
|
path = resource_name
|
|
182
|
-
|
|
183
|
+
|
|
184
|
+
# Usamos Rack para serializar anidamiento (q[service]=val)
|
|
185
|
+
path += "?#{Rack::Utils.build_nested_query(filters)}" if filters.present?
|
|
183
186
|
|
|
184
187
|
response = bug_bunny_client.request(path, method: :get, exchange: current_exchange, exchange_type: current_exchange_type, routing_key: rk)
|
|
185
188
|
|
|
186
189
|
return [] unless response['body'].is_a?(Array)
|
|
187
190
|
response['body'].map do |attrs|
|
|
188
|
-
# Al instanciar aquí, se captura el contexto si estamos dentro de un .with
|
|
189
191
|
inst = new(attrs)
|
|
190
192
|
inst.persisted = true
|
|
191
193
|
inst.send(:clear_changes_information)
|
|
@@ -204,7 +206,7 @@ module BugBunny
|
|
|
204
206
|
response = bug_bunny_client.request(path, method: :get, exchange: current_exchange, exchange_type: current_exchange_type, routing_key: rk)
|
|
205
207
|
|
|
206
208
|
return nil if response.nil? || response['status'] == 404
|
|
207
|
-
|
|
209
|
+
|
|
208
210
|
attributes = response['body']
|
|
209
211
|
return nil unless attributes.is_a?(Hash)
|
|
210
212
|
|
|
@@ -235,12 +237,12 @@ module BugBunny
|
|
|
235
237
|
def initialize(attributes = {})
|
|
236
238
|
@remote_attributes = {}.with_indifferent_access
|
|
237
239
|
@persisted = false
|
|
238
|
-
|
|
240
|
+
|
|
239
241
|
# === CAPTURA DE CONTEXTO ===
|
|
240
242
|
@routing_key = self.class.thread_config(:routing_key)
|
|
241
243
|
@exchange = self.class.thread_config(:exchange)
|
|
242
244
|
@exchange_type = self.class.thread_config(:exchange_type)
|
|
243
|
-
|
|
245
|
+
|
|
244
246
|
assign_attributes(attributes)
|
|
245
247
|
super()
|
|
246
248
|
end
|
|
@@ -269,7 +271,7 @@ module BugBunny
|
|
|
269
271
|
return if new_attributes.nil?
|
|
270
272
|
new_attributes.each { |k, v| public_send("#{k}=", v) }
|
|
271
273
|
end
|
|
272
|
-
|
|
274
|
+
|
|
273
275
|
def update(attributes)
|
|
274
276
|
assign_attributes(attributes)
|
|
275
277
|
save
|
|
@@ -325,10 +327,10 @@ module BugBunny
|
|
|
325
327
|
run_callbacks(:save) do
|
|
326
328
|
is_new = !persisted?
|
|
327
329
|
rk = calculate_routing_key(id)
|
|
328
|
-
|
|
330
|
+
|
|
329
331
|
# 1. Obtenemos el payload plano (atributos modificados)
|
|
330
332
|
flat_payload = changes_to_send
|
|
331
|
-
|
|
333
|
+
|
|
332
334
|
# 2. Wrappeamos automáticamente en la clave del modelo
|
|
333
335
|
key = self.class.param_key
|
|
334
336
|
wrapped_payload = { key => flat_payload }
|
data/lib/bug_bunny/version.rb
CHANGED
data/lib/bug_bunny.rb
CHANGED
|
@@ -13,6 +13,7 @@ require_relative 'bug_bunny/resource'
|
|
|
13
13
|
require_relative 'bug_bunny/rabbit'
|
|
14
14
|
require_relative 'bug_bunny/consumer'
|
|
15
15
|
require_relative 'bug_bunny/controller'
|
|
16
|
+
require_relative 'bug_bunny/middleware'
|
|
16
17
|
|
|
17
18
|
require_relative 'bug_bunny/middleware/stack'
|
|
18
19
|
require_relative 'bug_bunny/middleware/raise_error'
|
data/test_resource.rb
CHANGED
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
require_relative 'test_helper'
|
|
2
2
|
|
|
3
3
|
class TestUser < BugBunny::Resource
|
|
4
|
-
#
|
|
5
|
-
self.connection_pool = -> {
|
|
6
|
-
nil || TEST_POOL
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
# El exchange cambia según el entorno
|
|
10
|
-
self.exchange = -> {
|
|
11
|
-
ENV['IS_STAGING'] ? 'test_exchange' : 'test_exchange'
|
|
12
|
-
}
|
|
4
|
+
# Configuración del Pool
|
|
5
|
+
self.connection_pool = -> { nil || TEST_POOL }
|
|
13
6
|
|
|
7
|
+
# Configuración del Exchange
|
|
8
|
+
self.exchange = -> { ENV['IS_STAGING'] ? 'test_exchange' : 'test_exchange' }
|
|
14
9
|
self.exchange_type = 'topic'
|
|
15
|
-
self.routing_key_prefix = 'test_user'
|
|
16
10
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
11
|
+
# ACTUALIZADO v3.0: Usamos resource_name
|
|
12
|
+
self.resource_name = 'test_users'
|
|
13
|
+
|
|
14
|
+
# ELIMINADO: attribute :id, :integer (Causa crash en v3)
|
|
15
|
+
# ELIMINADO: attribute :name, :string (Causa crash en v3)
|
|
20
16
|
|
|
17
|
+
# Las validaciones siguen funcionando igual
|
|
21
18
|
validates :name, presence: true
|
|
22
19
|
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: 3.0.
|
|
4
|
+
version: 3.0.4
|
|
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-02-16 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bunny
|
|
@@ -200,6 +200,7 @@ files:
|
|
|
200
200
|
- lib/bug_bunny/consumer.rb
|
|
201
201
|
- lib/bug_bunny/controller.rb
|
|
202
202
|
- lib/bug_bunny/exception.rb
|
|
203
|
+
- lib/bug_bunny/middleware.rb
|
|
203
204
|
- lib/bug_bunny/middleware/json_response.rb
|
|
204
205
|
- lib/bug_bunny/middleware/raise_error.rb
|
|
205
206
|
- lib/bug_bunny/middleware/stack.rb
|