bug_bunny 3.0.2 → 3.0.3
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 +6 -0
- data/bin_suite.rb +41 -12
- data/lib/bug_bunny/consumer.rb +95 -53
- data/lib/bug_bunny/resource.rb +21 -19
- data/lib/bug_bunny/version.rb +1 -1
- data/test_resource.rb +10 -13
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6d012f3384230432e8f94c365d2a19ea05179f362f0433482c8d9967c3838102
|
|
4
|
+
data.tar.gz: ae306fdebe3c63b08a4980337294e46830a0277c9deea509a485d9978b2a9760
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fdee3c4b42d2e852649985f962fe81ca0cf3b152357d484e3e37310bbacd07889c82408c92b3b99936c0f81d2cf60e22ca57bae24f54e30efb0a36040649382b
|
|
7
|
+
data.tar.gz: 4c47cde21b912b1cad5cda132af21cb7ea5fcb0a7cefd8827f572695fbe50d6945b6083738e81fa8cc3f40b03344314f28655586e7c298925b2bf9482034acaa
|
data/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
|
+
## [3.0.3] - 2026-02-13
|
|
3
|
+
|
|
4
|
+
### 🐛 Bug Fixes
|
|
5
|
+
* **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.
|
|
6
|
+
* **Resource:** Now uses `Rack::Utils.build_nested_query` to generate correct URLs (e.g., `?q[service]=rabbit`).
|
|
7
|
+
* **Consumer:** Now uses `Rack::Utils.parse_nested_query` to correctly reconstruct nested hashes from the query string.
|
|
2
8
|
|
|
3
9
|
## [3.0.2] - 2026-02-12
|
|
4
10
|
|
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
|
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/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.3
|
|
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-13 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bunny
|