bug_bunny 3.0.1 → 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 +27 -0
- data/README.md +166 -197
- data/bin_suite.rb +41 -12
- data/lib/bug_bunny/config.rb +10 -0
- data/lib/bug_bunny/consumer.rb +116 -50
- data/lib/bug_bunny/controller.rb +131 -57
- data/lib/bug_bunny/producer.rb +10 -2
- data/lib/bug_bunny/resource.rb +140 -38
- data/lib/bug_bunny/version.rb +1 -1
- data/lib/bug_bunny.rb +1 -1
- data/test_resource.rb +10 -13
- metadata +21 -19
data/lib/bug_bunny/consumer.rb
CHANGED
|
@@ -4,24 +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
|
-
#
|
|
12
|
-
#
|
|
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.
|
|
18
|
+
#
|
|
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
|
+
# )
|
|
13
27
|
class Consumer
|
|
28
|
+
# @return [BugBunny::Session] La sesión wrapper de RabbitMQ que gestiona el canal.
|
|
14
29
|
attr_reader :session
|
|
15
30
|
|
|
31
|
+
# Método de conveniencia para instanciar y suscribir en un solo paso.
|
|
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.
|
|
16
36
|
def self.subscribe(connection:, **args)
|
|
17
37
|
new(connection).subscribe(**args)
|
|
18
38
|
end
|
|
19
39
|
|
|
40
|
+
# Inicializa un nuevo consumidor.
|
|
41
|
+
#
|
|
42
|
+
# @param connection [Bunny::Session] Conexión nativa de Bunny.
|
|
20
43
|
def initialize(connection)
|
|
21
44
|
@session = BugBunny::Session.new(connection)
|
|
22
45
|
end
|
|
23
46
|
|
|
24
|
-
# Inicia la suscripción a la cola.
|
|
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.
|
|
51
|
+
#
|
|
52
|
+
# @param queue_name [String] Nombre de la cola a escuchar.
|
|
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]
|
|
25
59
|
def subscribe(queue_name:, exchange_name:, routing_key:, exchange_type: 'direct', queue_opts: {}, block: true)
|
|
26
60
|
x = session.exchange(name: exchange_name, type: exchange_type)
|
|
27
61
|
q = session.queue(queue_name, queue_opts)
|
|
@@ -41,20 +75,25 @@ module BugBunny
|
|
|
41
75
|
|
|
42
76
|
private
|
|
43
77
|
|
|
44
|
-
# Procesa
|
|
45
|
-
#
|
|
78
|
+
# Procesa un mensaje individual recibido de la cola.
|
|
79
|
+
#
|
|
80
|
+
# Realiza la orquestación completa: Parsing -> Routing -> Ejecución -> Respuesta.
|
|
81
|
+
#
|
|
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]
|
|
46
86
|
def process_message(delivery_info, properties, body)
|
|
47
87
|
if properties.type.nil? || properties.type.empty?
|
|
48
|
-
BugBunny.configuration.logger.error("[Consumer] Missing 'type'.
|
|
88
|
+
BugBunny.configuration.logger.error("[Consumer] Missing 'type' header. Message rejected.")
|
|
49
89
|
session.channel.reject(delivery_info.delivery_tag, false)
|
|
50
90
|
return
|
|
51
91
|
end
|
|
52
92
|
|
|
53
|
-
# 1.
|
|
54
|
-
# Nota: Bunny devuelve los headers en propiedades.headers
|
|
93
|
+
# 1. Determinar Verbo HTTP (Default: GET)
|
|
55
94
|
http_method = properties.headers ? (properties.headers['x-http-method'] || 'GET') : 'GET'
|
|
56
95
|
|
|
57
|
-
# 2.
|
|
96
|
+
# 2. Router: Inferencia de Controlador y Acción
|
|
58
97
|
route_info = router_dispatch(http_method, properties.type)
|
|
59
98
|
|
|
60
99
|
headers = {
|
|
@@ -69,78 +108,86 @@ module BugBunny
|
|
|
69
108
|
reply_to: properties.reply_to
|
|
70
109
|
}
|
|
71
110
|
|
|
72
|
-
#
|
|
111
|
+
# 3. Instanciación Dinámica del Controlador
|
|
112
|
+
# Ej: "users" -> Rabbit::Controllers::UsersController
|
|
73
113
|
controller_class_name = "rabbit/controllers/#{route_info[:controller]}".camelize
|
|
74
114
|
controller_class = controller_class_name.constantize
|
|
75
115
|
|
|
116
|
+
# 4. Ejecución del Pipeline (Filtros -> Acción)
|
|
76
117
|
response_payload = controller_class.call(headers: headers, body: body)
|
|
77
118
|
|
|
119
|
+
# 5. Respuesta RPC (Si se solicita respuesta)
|
|
78
120
|
if properties.reply_to
|
|
79
121
|
reply(response_payload, properties.reply_to, properties.correlation_id)
|
|
80
122
|
end
|
|
81
123
|
|
|
124
|
+
# 6. Acknowledge (Confirmación de procesado)
|
|
82
125
|
session.channel.ack(delivery_info.delivery_tag)
|
|
126
|
+
|
|
83
127
|
rescue NameError => e
|
|
84
|
-
|
|
128
|
+
# Error 501/404: El controlador o la acción no existen.
|
|
129
|
+
BugBunny.configuration.logger.error("[Consumer] Routing Error: #{e.message}")
|
|
130
|
+
handle_fatal_error(properties, 501, "Routing Error", e.message)
|
|
85
131
|
session.channel.reject(delivery_info.delivery_tag, false)
|
|
132
|
+
|
|
86
133
|
rescue StandardError => e
|
|
134
|
+
# Error 500: Crash interno de la aplicación.
|
|
87
135
|
BugBunny.configuration.logger.error("[Consumer] Execution Error: #{e.message}")
|
|
136
|
+
handle_fatal_error(properties, 500, "Internal Server Error", e.message)
|
|
88
137
|
session.channel.reject(delivery_info.delivery_tag, false)
|
|
89
|
-
if properties.reply_to
|
|
90
|
-
reply({ error: e.message }, properties.reply_to, properties.correlation_id)
|
|
91
|
-
end
|
|
92
138
|
end
|
|
93
139
|
|
|
94
|
-
#
|
|
140
|
+
# Interpreta la URL y el verbo para decidir qué controlador ejecutar.
|
|
141
|
+
#
|
|
142
|
+
# Utiliza `Rack::Utils.parse_nested_query` para soportar parámetros anidados
|
|
143
|
+
# como `q[service]=rabbit`.
|
|
95
144
|
#
|
|
96
145
|
# @param method [String] Verbo HTTP (GET, POST, etc).
|
|
97
|
-
# @param path [String] URL
|
|
98
|
-
# @return [Hash] {controller, action, id, params}
|
|
146
|
+
# @param path [String] URL virtual del recurso (ej: 'users/1?active=true').
|
|
147
|
+
# @return [Hash] Estructura con keys {:controller, :action, :id, :params}.
|
|
99
148
|
def router_dispatch(method, path)
|
|
149
|
+
# Usamos URI para separar path de query string
|
|
100
150
|
uri = URI.parse("http://dummy/#{path}")
|
|
101
|
-
segments = uri.path.split('/').reject(&:empty?)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
#
|
|
112
|
-
|
|
151
|
+
segments = uri.path.split('/').reject(&:empty?)
|
|
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
|
|
162
|
+
controller_name = segments[0]
|
|
163
|
+
id = segments[1]
|
|
164
|
+
|
|
113
165
|
action = case method.to_s.upcase
|
|
114
|
-
when 'GET'
|
|
115
|
-
|
|
116
|
-
when '
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
'update'
|
|
120
|
-
when 'DELETE'
|
|
121
|
-
'destroy'
|
|
122
|
-
else
|
|
123
|
-
id || 'index' # Fallback para verbos custom
|
|
166
|
+
when 'GET' then id ? 'show' : 'index'
|
|
167
|
+
when 'POST' then 'create'
|
|
168
|
+
when 'PUT', 'PATCH' then 'update'
|
|
169
|
+
when 'DELETE' then 'destroy'
|
|
170
|
+
else id || 'index'
|
|
124
171
|
end
|
|
125
172
|
|
|
126
|
-
# Soporte para
|
|
127
|
-
# Path: users/1/activate -> segments: [users, 1, activate]
|
|
173
|
+
# Soporte para rutas miembro custom (POST users/1/promote)
|
|
128
174
|
if segments.size >= 3
|
|
129
175
|
id = segments[1]
|
|
130
176
|
action = segments[2]
|
|
131
177
|
end
|
|
132
178
|
|
|
133
|
-
#
|
|
179
|
+
# Inyectamos el ID en los params si existe en la ruta
|
|
134
180
|
query_params['id'] = id if id
|
|
135
181
|
|
|
136
|
-
{
|
|
137
|
-
controller: controller_name,
|
|
138
|
-
action: action,
|
|
139
|
-
id: id,
|
|
140
|
-
params: query_params
|
|
141
|
-
}
|
|
182
|
+
{ controller: controller_name, action: action, id: id, params: query_params }
|
|
142
183
|
end
|
|
143
184
|
|
|
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]
|
|
144
191
|
def reply(payload, reply_to, correlation_id)
|
|
145
192
|
session.channel.default_exchange.publish(
|
|
146
193
|
payload.to_json,
|
|
@@ -150,10 +197,29 @@ module BugBunny
|
|
|
150
197
|
)
|
|
151
198
|
end
|
|
152
199
|
|
|
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.
|
|
153
218
|
def start_health_check(q_name)
|
|
154
|
-
Concurrent::TimerTask.new(execution_interval:
|
|
219
|
+
Concurrent::TimerTask.new(execution_interval: BugBunny.configuration.health_check_interval) do
|
|
155
220
|
session.channel.queue_declare(q_name, passive: true)
|
|
156
221
|
rescue StandardError
|
|
222
|
+
BugBunny.configuration.logger.warn("[Consumer] Queue check failed. Reconnecting session...")
|
|
157
223
|
session.close
|
|
158
224
|
end.execute
|
|
159
225
|
end
|
data/lib/bug_bunny/controller.rb
CHANGED
|
@@ -6,109 +6,183 @@ module BugBunny
|
|
|
6
6
|
# Clase base para Controladores de Mensajes.
|
|
7
7
|
#
|
|
8
8
|
# Provee una abstracción similar a ActionController para manejar peticiones RPC.
|
|
9
|
-
#
|
|
10
|
-
#
|
|
9
|
+
# Incluye soporte para `before_action`, manejo de excepciones declarativo (`rescue_from`)
|
|
10
|
+
# y normalización de parámetros.
|
|
11
11
|
class Controller
|
|
12
12
|
include ActiveModel::Model
|
|
13
13
|
include ActiveModel::Attributes
|
|
14
14
|
|
|
15
|
-
# @return [Hash] Metadatos
|
|
15
|
+
# @return [Hash] Metadatos del mensaje (headers AMQP, routing info).
|
|
16
16
|
attribute :headers
|
|
17
17
|
|
|
18
|
-
# @return [ActiveSupport::HashWithIndifferentAccess] Parámetros unificados.
|
|
18
|
+
# @return [ActiveSupport::HashWithIndifferentAccess] Parámetros unificados (Body + Query + Route).
|
|
19
19
|
attribute :params
|
|
20
20
|
|
|
21
|
-
# @return [String] Cuerpo crudo si no es JSON.
|
|
21
|
+
# @return [String] Cuerpo crudo si el payload no es JSON.
|
|
22
22
|
attribute :raw_string
|
|
23
23
|
|
|
24
|
-
# @return [Hash]
|
|
24
|
+
# @return [Hash, nil] La respuesta renderizada { status, body }.
|
|
25
25
|
attr_reader :rendered_response
|
|
26
26
|
|
|
27
|
+
# --- INFRAESTRUCTURA DE FILTROS (Before Actions) ---
|
|
28
|
+
|
|
27
29
|
# @api private
|
|
28
30
|
def self.before_actions
|
|
29
|
-
@before_actions ||= Hash.new { |
|
|
31
|
+
@before_actions ||= Hash.new { |h, k| h[k] = [] }
|
|
30
32
|
end
|
|
31
33
|
|
|
32
|
-
# Registra un callback
|
|
34
|
+
# Registra un callback que se ejecutará antes de las acciones.
|
|
35
|
+
#
|
|
36
|
+
# @param method_name [Symbol] Nombre del método a ejecutar.
|
|
37
|
+
# @param options [Hash] Opciones de filtro (:only).
|
|
38
|
+
# @example
|
|
39
|
+
# before_action :set_user, only: [:show, :update]
|
|
33
40
|
def self.before_action(method_name, **options)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
before_actions[
|
|
41
|
+
only = Array(options[:only]).map(&:to_sym)
|
|
42
|
+
target_actions = only.empty? ? [:_all_actions] : only
|
|
43
|
+
|
|
44
|
+
target_actions.each do |action|
|
|
45
|
+
before_actions[action] << method_name
|
|
39
46
|
end
|
|
40
47
|
end
|
|
41
48
|
|
|
42
|
-
#
|
|
43
|
-
|
|
44
|
-
# @
|
|
49
|
+
# --- INFRAESTRUCTURA DE MANEJO DE ERRORES (Rescue From) ---
|
|
50
|
+
|
|
51
|
+
# @api private
|
|
52
|
+
def self.rescue_handlers
|
|
53
|
+
@rescue_handlers ||= []
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Registra un manejador para una o más excepciones.
|
|
57
|
+
# Los manejadores se evalúan en orden inverso (el último registrado tiene prioridad).
|
|
58
|
+
#
|
|
59
|
+
# @param klasses [Class] Clases de excepción a capturar.
|
|
60
|
+
# @param with [Symbol] Nombre del método manejador.
|
|
61
|
+
# @param block [Proc] Bloque manejador.
|
|
62
|
+
#
|
|
63
|
+
# @example Con método
|
|
64
|
+
# rescue_from User::NotAuthorized, with: :deny_access
|
|
65
|
+
#
|
|
66
|
+
# @example Con bloque
|
|
67
|
+
# rescue_from ActiveRecord::RecordNotFound do |e|
|
|
68
|
+
# render status: :not_found, json: { error: e.message }
|
|
69
|
+
# end
|
|
70
|
+
def self.rescue_from(*klasses, with: nil, &block)
|
|
71
|
+
handler = with || block
|
|
72
|
+
raise ArgumentError, "Need a handler. Supply 'with: :method' or a block." unless handler
|
|
73
|
+
|
|
74
|
+
klasses.each do |klass|
|
|
75
|
+
# Insertamos al principio para que las últimas definiciones tengan prioridad (LIFO)
|
|
76
|
+
rescue_handlers.unshift([klass, handler])
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# --- PIPELINE DE EJECUCIÓN ---
|
|
81
|
+
|
|
82
|
+
# Punto de entrada principal llamado por el Consumer.
|
|
83
|
+
# Instancia el controlador y procesa el mensaje.
|
|
84
|
+
#
|
|
85
|
+
# @param headers [Hash] Metadatos del mensaje.
|
|
86
|
+
# @param body [Hash, String] Payload deserializado.
|
|
87
|
+
# @return [Hash] La respuesta final { status, body }.
|
|
45
88
|
def self.call(headers:, body: {})
|
|
46
|
-
|
|
47
|
-
|
|
89
|
+
new(headers: headers).process(body)
|
|
90
|
+
end
|
|
48
91
|
|
|
49
|
-
|
|
92
|
+
# Ejecuta el ciclo de vida de la petición: Params -> Filtros -> Acción.
|
|
93
|
+
# Captura cualquier error y delega al sistema `rescue_from`.
|
|
94
|
+
#
|
|
95
|
+
# @param body [Hash, String] Payload.
|
|
96
|
+
# @return [Hash] Respuesta RPC.
|
|
97
|
+
def process(body)
|
|
98
|
+
prepare_params(body)
|
|
99
|
+
action_name = headers[:action].to_sym
|
|
50
100
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
101
|
+
# 1. Ejecutar Before Actions (si retorna false, hubo render/halt)
|
|
102
|
+
return rendered_response unless run_before_actions(action_name)
|
|
103
|
+
|
|
104
|
+
# 2. Ejecutar Acción
|
|
105
|
+
if respond_to?(action_name)
|
|
106
|
+
public_send(action_name)
|
|
54
107
|
else
|
|
55
|
-
raise NameError, "Action '#{
|
|
108
|
+
raise NameError, "Action '#{action_name}' not found in #{self.class.name}"
|
|
56
109
|
end
|
|
57
110
|
|
|
58
|
-
|
|
111
|
+
# 3. Respuesta por defecto (204 No Content) si la acción no llamó a render
|
|
112
|
+
rendered_response || { status: 204, body: nil }
|
|
113
|
+
|
|
59
114
|
rescue StandardError => e
|
|
60
|
-
|
|
115
|
+
handle_exception(e)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
# Busca un manejador registrado para la excepción y lo ejecuta.
|
|
121
|
+
# Si no hay ninguno, loguea y devuelve 500.
|
|
122
|
+
def handle_exception(exception)
|
|
123
|
+
# Buscamos el primer handler compatible con la clase del error
|
|
124
|
+
handler_entry = self.class.rescue_handlers.find { |klass, _| exception.is_a?(klass) }
|
|
125
|
+
|
|
126
|
+
if handler_entry
|
|
127
|
+
_, handler = handler_entry
|
|
128
|
+
|
|
129
|
+
# Ejecutamos el handler en el contexto de la INSTANCIA
|
|
130
|
+
if handler.is_a?(Symbol)
|
|
131
|
+
send(handler, exception)
|
|
132
|
+
elsif handler.respond_to?(:call)
|
|
133
|
+
instance_exec(exception, &handler)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Si el handler hizo render, retornamos esa respuesta
|
|
137
|
+
return rendered_response if rendered_response
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# === FALLBACK POR DEFECTO ===
|
|
141
|
+
# Si el error no fue rescatado por el usuario, actuamos como red de seguridad.
|
|
142
|
+
BugBunny.configuration.logger.error("Controller Error (#{exception.class}): #{exception.message}")
|
|
143
|
+
BugBunny.configuration.logger.error(exception.backtrace.join("\n"))
|
|
144
|
+
|
|
145
|
+
{ status: 500, body: { error: exception.message, type: exception.class.name } }
|
|
61
146
|
end
|
|
62
147
|
|
|
63
|
-
#
|
|
64
|
-
|
|
65
|
-
|
|
148
|
+
# Ejecuta la cadena de filtros before_action.
|
|
149
|
+
def run_before_actions(action_name)
|
|
150
|
+
chain = (self.class.before_actions[:_all_actions] || []) +
|
|
151
|
+
(self.class.before_actions[action_name] || [])
|
|
152
|
+
|
|
153
|
+
chain.uniq.each do |method_name|
|
|
154
|
+
send(method_name)
|
|
155
|
+
# Si un filtro llamó a 'render', detenemos la cadena (halt)
|
|
156
|
+
return false if rendered_response
|
|
157
|
+
end
|
|
158
|
+
true
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Construye la respuesta RPC normalizada.
|
|
162
|
+
#
|
|
163
|
+
# @param status [Symbol, Integer] Código HTTP (ej: :ok, 200, :not_found).
|
|
164
|
+
# @param json [Object] Objeto a serializar en el body.
|
|
66
165
|
def render(status:, json: nil)
|
|
67
166
|
code = Rack::Utils::SYMBOL_TO_STATUS_CODE[status] || 200
|
|
68
167
|
@rendered_response = { status: code, body: json }
|
|
69
168
|
end
|
|
70
169
|
|
|
71
|
-
#
|
|
170
|
+
# Normaliza y fusiona parámetros de múltiples fuentes.
|
|
72
171
|
# Prioridad: Body > ID Ruta > Query Params.
|
|
73
|
-
#
|
|
74
|
-
# @param body [Hash, String] Payload.
|
|
75
172
|
def prepare_params(body)
|
|
76
|
-
self.params
|
|
77
|
-
|
|
78
|
-
# 1. Query Params (de la URL ?active=true)
|
|
79
|
-
if headers[:query_params].present?
|
|
80
|
-
params.merge!(headers[:query_params])
|
|
81
|
-
end
|
|
173
|
+
self.params = {}.with_indifferent_access
|
|
82
174
|
|
|
83
|
-
|
|
175
|
+
params.merge!(headers[:query_params]) if headers[:query_params].present?
|
|
84
176
|
params[:id] = headers[:id] if headers[:id].present?
|
|
85
177
|
|
|
86
|
-
# 3. Payload Body (JSON)
|
|
87
178
|
if body.is_a?(Hash)
|
|
88
179
|
params.merge!(body)
|
|
89
|
-
elsif body.is_a?(String) && headers[:content_type]
|
|
180
|
+
elsif body.is_a?(String) && headers[:content_type].to_s.include?('json')
|
|
90
181
|
parsed = JSON.parse(body) rescue nil
|
|
91
182
|
params.merge!(parsed) if parsed
|
|
92
183
|
else
|
|
93
184
|
self.raw_string = body
|
|
94
185
|
end
|
|
95
186
|
end
|
|
96
|
-
|
|
97
|
-
# Ejecuta callbacks. Retorna false si hubo `render` (halt).
|
|
98
|
-
# @api private
|
|
99
|
-
def run_callbacks
|
|
100
|
-
current = headers[:action].to_sym
|
|
101
|
-
chain = self.class.before_actions[:_all_actions] + self.class.before_actions[current]
|
|
102
|
-
chain.each do |method|
|
|
103
|
-
send(method)
|
|
104
|
-
return false if @rendered_response
|
|
105
|
-
end
|
|
106
|
-
true
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def self.rescue_from(e)
|
|
110
|
-
BugBunny.configuration.logger.error("Controller Error: #{e.message}")
|
|
111
|
-
{ status: 500, error: e.message }
|
|
112
|
-
end
|
|
113
187
|
end
|
|
114
188
|
end
|
data/lib/bug_bunny/producer.rb
CHANGED
|
@@ -37,7 +37,14 @@ module BugBunny
|
|
|
37
37
|
payload = serialize_message(request.body)
|
|
38
38
|
opts = request.amqp_options
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
# LOG ESTRUCTURADO Y LEGIBLE
|
|
41
|
+
# Muestra claramente: Verbo, Recurso, Exchange (y su tipo) y la Routing Key usada.
|
|
42
|
+
verb = request.method.to_s.upcase
|
|
43
|
+
target = request.path
|
|
44
|
+
ex_info = "'#{request.exchange}' (Type: #{request.exchange_type})"
|
|
45
|
+
rk = request.final_routing_key
|
|
46
|
+
|
|
47
|
+
BugBunny.configuration.logger.info("[BugBunny] [#{verb}] '/#{target}' | Exchange: #{ex_info} | Routing Key: '#{rk}'")
|
|
41
48
|
|
|
42
49
|
x.publish(payload, opts.merge(routing_key: request.final_routing_key))
|
|
43
50
|
end
|
|
@@ -76,7 +83,8 @@ module BugBunny
|
|
|
76
83
|
response_payload = future.value(wait_timeout)
|
|
77
84
|
|
|
78
85
|
if response_payload.nil?
|
|
79
|
-
|
|
86
|
+
# CORRECCIÓN: Usamos request.path y request.method en lugar de request.action
|
|
87
|
+
raise BugBunny::RequestTimeout, "Timeout waiting for RPC: #{request.path} [#{request.method}]"
|
|
80
88
|
end
|
|
81
89
|
|
|
82
90
|
parse_response(response_payload)
|