bug_bunny 3.1.0 → 3.1.2
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 +25 -0
- data/README.md +197 -345
- data/lib/bug_bunny/client.rb +26 -11
- data/lib/bug_bunny/configuration.rb +28 -2
- data/lib/bug_bunny/consumer.rb +27 -16
- data/lib/bug_bunny/controller.rb +142 -70
- data/lib/bug_bunny/producer.rb +51 -32
- data/lib/bug_bunny/request.rb +14 -2
- data/lib/bug_bunny/resource.rb +152 -18
- data/lib/bug_bunny/session.rb +47 -18
- data/lib/bug_bunny/version.rb +1 -1
- data/lib/bug_bunny.rb +1 -1
- data/test/integration/infrastructure_test.rb +61 -0
- data/test/integration/manual_client_test.rb +203 -0
- data/test/test_helper.rb +96 -11
- metadata +18 -16
- data/bin_client.rb +0 -51
- data/bin_suite.rb +0 -106
- data/bin_worker.rb +0 -26
- data/test/integration/fire_and_forget_test.rb +0 -76
- data/test/integration/rpc_flow_test.rb +0 -78
- data/test/unit/configuration_test.rb +0 -40
- data/test/unit/consumer_test.rb +0 -44
- data/test/unit/controller_headers_test.rb +0 -38
- data/test/unit/hybrid_resource_test.rb +0 -60
- data/test/unit/middleware_test.rb +0 -61
- data/test/unit/resource_test.rb +0 -49
- data/test_controller.rb +0 -49
- data/test_helper.rb +0 -20
- data/test_resource.rb +0 -19
data/lib/bug_bunny/client.rb
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
#
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
require_relative 'middleware/stack'
|
|
3
4
|
|
|
4
5
|
module BugBunny
|
|
5
6
|
# Cliente principal para realizar peticiones a RabbitMQ.
|
|
6
7
|
#
|
|
7
8
|
# Implementa el patrón "Onion Middleware" (Arquitectura de Cebolla) similar a Faraday.
|
|
8
|
-
# Mantiene una interfaz flexible donde el verbo HTTP se pasa como opción
|
|
9
|
+
# Mantiene una interfaz flexible donde el verbo HTTP se pasa como opción y permite
|
|
10
|
+
# configurar la infraestructura AMQP de forma granular por petición.
|
|
9
11
|
#
|
|
10
|
-
# @example Petición RPC (GET)
|
|
11
|
-
# client.request('users/123', method: :get)
|
|
12
|
+
# @example Petición RPC (GET) con opciones de infraestructura
|
|
13
|
+
# client.request('users/123', method: :get, exchange_options: { durable: true })
|
|
12
14
|
#
|
|
13
15
|
# @example Publicación Fire-and-Forget (POST)
|
|
14
16
|
# client.publish('logs', method: :post, body: { msg: 'Error' })
|
|
@@ -41,6 +43,8 @@ module BugBunny
|
|
|
41
43
|
# @option args [Object] :body El cuerpo del mensaje.
|
|
42
44
|
# @option args [Hash] :headers Headers AMQP adicionales.
|
|
43
45
|
# @option args [Integer] :timeout Tiempo máximo de espera.
|
|
46
|
+
# @option args [Hash] :exchange_options Opciones específicas para la declaración del Exchange.
|
|
47
|
+
# @option args [Hash] :queue_options Opciones específicas para la declaración de la Cola.
|
|
44
48
|
# @yield [req] Bloque para configurar el objeto Request directamente.
|
|
45
49
|
# @return [Hash] La respuesta del servidor.
|
|
46
50
|
def request(url, **args)
|
|
@@ -65,18 +69,28 @@ module BugBunny
|
|
|
65
69
|
|
|
66
70
|
# Ejecuta la lógica de envío dentro del contexto del Pool.
|
|
67
71
|
# Mapea los argumentos al objeto Request y ejecuta la cadena de middlewares.
|
|
72
|
+
#
|
|
73
|
+
# @param method_name [Symbol] El método del productor a llamar (:rpc o :fire).
|
|
74
|
+
# @param url [String] La ruta destino.
|
|
75
|
+
# @param args [Hash] Argumentos pasados a los métodos públicos.
|
|
76
|
+
# @yield [req] Bloque para configuración adicional del Request.
|
|
68
77
|
def run_in_pool(method_name, url, args)
|
|
69
78
|
# 1. Builder del Request
|
|
70
79
|
req = BugBunny::Request.new(url)
|
|
71
80
|
|
|
72
81
|
# 2. Syntactic Sugar: Mapeo de argumentos a atributos del Request
|
|
73
|
-
req.method
|
|
74
|
-
req.body
|
|
75
|
-
req.exchange
|
|
76
|
-
req.exchange_type
|
|
77
|
-
req.routing_key
|
|
78
|
-
req.timeout
|
|
79
|
-
|
|
82
|
+
req.method = args[:method] if args[:method]
|
|
83
|
+
req.body = args[:body] if args[:body]
|
|
84
|
+
req.exchange = args[:exchange] if args[:exchange]
|
|
85
|
+
req.exchange_type = args[:exchange_type] if args[:exchange_type]
|
|
86
|
+
req.routing_key = args[:routing_key] if args[:routing_key]
|
|
87
|
+
req.timeout = args[:timeout] if args[:timeout]
|
|
88
|
+
|
|
89
|
+
# Inyección de opciones de infraestructura (Nivel 3 de la cascada)
|
|
90
|
+
req.exchange_options = args[:exchange_options] if args[:exchange_options]
|
|
91
|
+
req.queue_options = args[:queue_options] if args[:queue_options]
|
|
92
|
+
|
|
93
|
+
req.headers.merge!(args[:headers]) if args[:headers]
|
|
80
94
|
|
|
81
95
|
# 3. Configuración del usuario (bloque específico por request)
|
|
82
96
|
yield req if block_given?
|
|
@@ -94,6 +108,7 @@ module BugBunny
|
|
|
94
108
|
app = @stack.build(final_action)
|
|
95
109
|
app.call(req)
|
|
96
110
|
ensure
|
|
111
|
+
# Aseguramos el cierre del canal pero mantenemos la conexión del pool
|
|
97
112
|
session.close
|
|
98
113
|
end
|
|
99
114
|
end
|
|
@@ -4,7 +4,14 @@ require 'logger'
|
|
|
4
4
|
|
|
5
5
|
module BugBunny
|
|
6
6
|
# Clase de configuración global para la gema BugBunny.
|
|
7
|
-
# Almacena las credenciales de conexión, timeouts y parámetros de ajuste de RabbitMQ
|
|
7
|
+
# Almacena las credenciales de conexión, timeouts y parámetros de ajuste de RabbitMQ,
|
|
8
|
+
# así como las opciones por defecto para la declaración de infraestructura AMQP.
|
|
9
|
+
#
|
|
10
|
+
# @example Configuración en un inicializador (e.g., config/initializers/bug_bunny.rb)
|
|
11
|
+
# BugBunny.configure do |config|
|
|
12
|
+
# config.host = '127.0.0.1'
|
|
13
|
+
# config.exchange_options = { durable: true, auto_delete: false }
|
|
14
|
+
# end
|
|
8
15
|
class Configuration
|
|
9
16
|
# @return [String] Host o IP del servidor RabbitMQ (ej: 'localhost').
|
|
10
17
|
attr_accessor :host
|
|
@@ -60,9 +67,23 @@ module BugBunny
|
|
|
60
67
|
# @return [String] Namespace base donde se buscarán los controladores (default: 'Rabbit::Controllers').
|
|
61
68
|
attr_accessor :controller_namespace
|
|
62
69
|
|
|
63
|
-
# @return [Array<Symbol, Proc, String>]
|
|
70
|
+
# @return [Array<Symbol, Proc, String>] Etiquetas para el log estructurado.
|
|
64
71
|
attr_accessor :log_tags
|
|
65
72
|
|
|
73
|
+
# @!group Configuración de Infraestructura Global
|
|
74
|
+
|
|
75
|
+
# @return [Hash] Opciones globales por defecto para la declaración de Exchanges.
|
|
76
|
+
# Estas opciones se fusionarán con los valores por defecto de la gema y las específicas del recurso.
|
|
77
|
+
# @example { durable: true, auto_delete: false }
|
|
78
|
+
attr_accessor :exchange_options
|
|
79
|
+
|
|
80
|
+
# @return [Hash] Opciones globales por defecto para la declaración de Colas.
|
|
81
|
+
# Estas opciones se fusionarán con los valores por defecto de la gema y las específicas del recurso.
|
|
82
|
+
# @example { durable: true, exclusive: false }
|
|
83
|
+
attr_accessor :queue_options
|
|
84
|
+
|
|
85
|
+
# @!endgroup
|
|
86
|
+
|
|
66
87
|
# Inicializa la configuración con valores por defecto seguros.
|
|
67
88
|
def initialize
|
|
68
89
|
@host = '127.0.0.1'
|
|
@@ -91,9 +112,14 @@ module BugBunny
|
|
|
91
112
|
@controller_namespace = 'Rabbit::Controllers'
|
|
92
113
|
|
|
93
114
|
@log_tags = [:uuid]
|
|
115
|
+
|
|
116
|
+
# Inicialización de opciones de infraestructura como hashes vacíos para permitir fusiones posteriores.
|
|
117
|
+
@exchange_options = {}
|
|
118
|
+
@queue_options = {}
|
|
94
119
|
end
|
|
95
120
|
|
|
96
121
|
# Construye la URL de conexión AMQP basada en los atributos configurados.
|
|
122
|
+
# @return [String] URL formateada amqp://user:pass@host:port/vhost
|
|
97
123
|
def url
|
|
98
124
|
"amqp://#{username}:#{password}@#{host}:#{port}/#{vhost}"
|
|
99
125
|
end
|
data/lib/bug_bunny/consumer.rb
CHANGED
|
@@ -53,15 +53,27 @@ module BugBunny
|
|
|
53
53
|
# @param exchange_name [String] Nombre del exchange al cual enlazar la cola.
|
|
54
54
|
# @param routing_key [String] Patrón de enrutamiento (ej: 'users.*').
|
|
55
55
|
# @param exchange_type [String] Tipo de exchange ('direct', 'topic', 'fanout').
|
|
56
|
+
# @param exchange_opts [Hash] Opciones adicionales para el exchange (durable, auto_delete).
|
|
56
57
|
# @param queue_opts [Hash] Opciones adicionales para la cola (durable, auto_delete).
|
|
57
58
|
# @param block [Boolean] Si es `true`, bloquea el hilo actual (loop infinito).
|
|
58
59
|
# @return [void]
|
|
59
|
-
def subscribe(queue_name:, exchange_name:, routing_key:, exchange_type: 'direct', queue_opts: {}, block: true)
|
|
60
|
-
|
|
60
|
+
def subscribe(queue_name:, exchange_name:, routing_key:, exchange_type: 'direct', exchange_opts: {}, queue_opts: {}, block: true)
|
|
61
|
+
# Declaración de Infraestructura
|
|
62
|
+
x = session.exchange(name: exchange_name, type: exchange_type, opts: exchange_opts)
|
|
61
63
|
q = session.queue(queue_name, queue_opts)
|
|
62
64
|
q.bind(x, routing_key: routing_key)
|
|
63
65
|
|
|
64
|
-
|
|
66
|
+
# 📊 LOGGING DE OBSERVABILIDAD: Calculamos las opciones finales para mostrarlas en consola
|
|
67
|
+
final_x_opts = BugBunny::Session::DEFAULT_EXCHANGE_OPTIONS
|
|
68
|
+
.merge(BugBunny.configuration.exchange_options || {})
|
|
69
|
+
.merge(exchange_opts || {})
|
|
70
|
+
final_q_opts = BugBunny::Session::DEFAULT_QUEUE_OPTIONS
|
|
71
|
+
.merge(BugBunny.configuration.queue_options || {})
|
|
72
|
+
.merge(queue_opts || {})
|
|
73
|
+
|
|
74
|
+
BugBunny.configuration.logger.info("[BugBunny::Consumer] 🎧 Listening on '#{queue_name}' (Opts: #{final_q_opts})")
|
|
75
|
+
BugBunny.configuration.logger.info("[BugBunny::Consumer] 🔀 Bounded to Exchange '#{exchange_name}' (#{exchange_type}) | Opts: #{final_x_opts} | RK: '#{routing_key}'")
|
|
76
|
+
|
|
65
77
|
start_health_check(queue_name)
|
|
66
78
|
|
|
67
79
|
q.subscribe(manual_ack: true, block: block) do |delivery_info, properties, body|
|
|
@@ -78,7 +90,7 @@ module BugBunny
|
|
|
78
90
|
end
|
|
79
91
|
end
|
|
80
92
|
rescue StandardError => e
|
|
81
|
-
BugBunny.configuration.logger.error("[Consumer] Connection Error: #{e.message}. Retrying...")
|
|
93
|
+
BugBunny.configuration.logger.error("[BugBunny::Consumer] 💥 Connection Error: #{e.message}. Retrying in #{BugBunny.configuration.network_recovery_interval}s...")
|
|
82
94
|
sleep BugBunny.configuration.network_recovery_interval
|
|
83
95
|
retry
|
|
84
96
|
end
|
|
@@ -94,15 +106,11 @@ module BugBunny
|
|
|
94
106
|
# @param body [String] El payload crudo del mensaje.
|
|
95
107
|
# @return [void]
|
|
96
108
|
def process_message(delivery_info, properties, body)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
path = properties.type
|
|
100
|
-
if path.nil? || path.empty?
|
|
101
|
-
path = properties.headers ? properties.headers['path'] : nil
|
|
102
|
-
end
|
|
109
|
+
# 1. Validación de Headers
|
|
110
|
+
path = properties.type || (properties.headers && properties.headers['path'])
|
|
103
111
|
|
|
104
112
|
if path.nil? || path.empty?
|
|
105
|
-
BugBunny.configuration.logger.error("[Consumer] Missing 'type'
|
|
113
|
+
BugBunny.configuration.logger.error("[BugBunny::Consumer] ⛔ Rejected: Missing 'type' header.")
|
|
106
114
|
session.channel.reject(delivery_info.delivery_tag, false)
|
|
107
115
|
return
|
|
108
116
|
end
|
|
@@ -114,6 +122,9 @@ module BugBunny
|
|
|
114
122
|
# 3. Router: Inferencia de Controlador y Acción
|
|
115
123
|
route_info = router_dispatch(http_method, path)
|
|
116
124
|
|
|
125
|
+
BugBunny.configuration.logger.info("[BugBunny::Consumer] 📥 Started #{http_method} \"/#{path}\" for Routing Key: #{delivery_info.routing_key}")
|
|
126
|
+
BugBunny.configuration.logger.debug("[BugBunny::Consumer] 📦 Body: #{body.truncate(200)}")
|
|
127
|
+
|
|
117
128
|
request_metadata = {
|
|
118
129
|
type: path,
|
|
119
130
|
http_method: http_method,
|
|
@@ -133,7 +144,7 @@ module BugBunny
|
|
|
133
144
|
controller_name = route_info[:controller].camelize
|
|
134
145
|
|
|
135
146
|
# Construcción: "Messaging::Handlers" + "::" + "Users"
|
|
136
|
-
controller_class_name = "#{namespace}::#{controller_name}"
|
|
147
|
+
controller_class_name = "#{namespace}::#{controller_name}Controller"
|
|
137
148
|
|
|
138
149
|
controller_class = controller_class_name.constantize
|
|
139
150
|
|
|
@@ -141,7 +152,7 @@ module BugBunny
|
|
|
141
152
|
raise BugBunny::SecurityError, "Class #{controller_class} is not a valid BugBunny Controller"
|
|
142
153
|
end
|
|
143
154
|
rescue NameError => _e
|
|
144
|
-
BugBunny.configuration.logger.
|
|
155
|
+
BugBunny.configuration.logger.warn("[BugBunny::Consumer] ⚠️ Controller not found: #{controller_class_name} (Path: #{path})")
|
|
145
156
|
handle_fatal_error(properties, 404, "Not Found", "Controller #{controller_class_name} not found")
|
|
146
157
|
session.channel.reject(delivery_info.delivery_tag, false)
|
|
147
158
|
return
|
|
@@ -159,7 +170,7 @@ module BugBunny
|
|
|
159
170
|
session.channel.ack(delivery_info.delivery_tag)
|
|
160
171
|
|
|
161
172
|
rescue StandardError => e
|
|
162
|
-
BugBunny.configuration.logger.error("[Consumer] Execution Error: #{e.message}")
|
|
173
|
+
BugBunny.configuration.logger.error("[BugBunny::Consumer] 💥 Execution Error (#{e.class}): #{e.message}")
|
|
163
174
|
handle_fatal_error(properties, 500, "Internal Server Error", e.message)
|
|
164
175
|
session.channel.reject(delivery_info.delivery_tag, false)
|
|
165
176
|
end
|
|
@@ -216,7 +227,7 @@ module BugBunny
|
|
|
216
227
|
# @param correlation_id [String] ID para correlacionar la respuesta con la petición original.
|
|
217
228
|
# @return [void]
|
|
218
229
|
def reply(payload, reply_to, correlation_id)
|
|
219
|
-
BugBunny.configuration.logger.debug("[Consumer] 📤
|
|
230
|
+
BugBunny.configuration.logger.debug("[BugBunny::Consumer] 📤 Sending RPC Reply to #{reply_to} | ID: #{correlation_id}")
|
|
220
231
|
session.channel.default_exchange.publish(
|
|
221
232
|
payload.to_json,
|
|
222
233
|
routing_key: reply_to,
|
|
@@ -247,7 +258,7 @@ module BugBunny
|
|
|
247
258
|
Concurrent::TimerTask.new(execution_interval: BugBunny.configuration.health_check_interval) do
|
|
248
259
|
session.channel.queue_declare(q_name, passive: true)
|
|
249
260
|
rescue StandardError
|
|
250
|
-
BugBunny.configuration.logger.warn("[Consumer] Queue check failed. Reconnecting session...")
|
|
261
|
+
BugBunny.configuration.logger.warn("[BugBunny::Consumer] ⚠️ Queue check failed. Reconnecting session...")
|
|
251
262
|
session.close
|
|
252
263
|
end.execute
|
|
253
264
|
end
|
data/lib/bug_bunny/controller.rb
CHANGED
|
@@ -7,99 +7,146 @@ require 'active_support/core_ext/class/attribute'
|
|
|
7
7
|
module BugBunny
|
|
8
8
|
# Clase base para todos los Controladores de Mensajes en BugBunny.
|
|
9
9
|
#
|
|
10
|
+
# Actúa como el receptor final de los mensajes enrutados desde el consumidor.
|
|
11
|
+
# Implementa un ciclo de vida similar a ActionController en Rails, soportando:
|
|
12
|
+
# - Filtros (`before_action`, `around_action`).
|
|
13
|
+
# - Manejo declarativo de errores (`rescue_from`).
|
|
14
|
+
# - Parsing de parámetros unificados (`params`).
|
|
15
|
+
# - Respuestas estructuradas (`render`).
|
|
16
|
+
#
|
|
10
17
|
# @author Gabriel
|
|
11
18
|
# @since 3.0.6
|
|
12
19
|
class Controller
|
|
13
20
|
include ActiveModel::Model
|
|
14
21
|
include ActiveModel::Attributes
|
|
15
22
|
|
|
16
|
-
#
|
|
23
|
+
# @!group Atributos de Instancia
|
|
24
|
+
|
|
25
|
+
# @return [Hash] Metadatos del mensaje entrante (ej. HTTP method, routing_key, id).
|
|
17
26
|
attribute :headers
|
|
18
27
|
|
|
19
|
-
# @return [ActiveSupport::HashWithIndifferentAccess] Parámetros unificados.
|
|
28
|
+
# @return [ActiveSupport::HashWithIndifferentAccess] Parámetros unificados (Body JSON + Query String).
|
|
20
29
|
attribute :params
|
|
21
30
|
|
|
22
|
-
# @return [String] Cuerpo crudo.
|
|
31
|
+
# @return [String] Cuerpo crudo original en caso de no ser JSON.
|
|
23
32
|
attribute :raw_string
|
|
24
33
|
|
|
25
|
-
# @return [Hash] Headers de respuesta.
|
|
34
|
+
# @return [Hash] Headers de respuesta que serán enviados de vuelta en RPC.
|
|
26
35
|
attr_reader :response_headers
|
|
27
36
|
|
|
28
|
-
# @return [Hash, nil] Respuesta renderizada.
|
|
37
|
+
# @return [Hash, nil] Respuesta final renderizada.
|
|
29
38
|
attr_reader :rendered_response
|
|
30
39
|
|
|
31
|
-
#
|
|
32
|
-
# Deben definirse ANTES de ser usados por la configuración de logs.
|
|
40
|
+
# @!endgroup
|
|
33
41
|
|
|
34
|
-
# @api private
|
|
35
|
-
def self.before_actions
|
|
36
|
-
@before_actions ||= Hash.new { |h, k| h[k] = [] }
|
|
37
|
-
end
|
|
38
42
|
|
|
39
|
-
#
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
# ==========================================
|
|
44
|
+
# INFRAESTRUCTURA DE FILTROS Y LOGS (HEREDABLES)
|
|
45
|
+
# ==========================================
|
|
46
|
+
|
|
47
|
+
# Usamos `class_attribute` con `default` para garantizar la herencia correcta
|
|
48
|
+
# hacia las subclases (ej. de ApplicationController a ServicesController).
|
|
49
|
+
class_attribute :before_actions, default: {}
|
|
50
|
+
class_attribute :around_actions, default: {}
|
|
51
|
+
class_attribute :log_tags, default: []
|
|
52
|
+
class_attribute :rescue_handlers, default: []
|
|
43
53
|
|
|
44
54
|
# Registra un filtro que se ejecutará **antes** de la acción.
|
|
55
|
+
# Si el filtro invoca `render`, la cadena se interrumpe y la acción no se ejecuta.
|
|
56
|
+
#
|
|
57
|
+
# @param method_name [Symbol] Nombre del método privado a ejecutar.
|
|
58
|
+
# @param options [Hash] Opciones como `only: [:show, :update]`.
|
|
59
|
+
# @return [void]
|
|
45
60
|
def self.before_action(method_name, **options)
|
|
46
|
-
register_callback(before_actions, method_name, options)
|
|
61
|
+
register_callback(:before_actions, method_name, options)
|
|
47
62
|
end
|
|
48
63
|
|
|
49
64
|
# Registra un filtro que **envuelve** la ejecución de la acción.
|
|
65
|
+
# El método registrado debe invocar `yield` para continuar la ejecución.
|
|
66
|
+
#
|
|
67
|
+
# @param method_name [Symbol] Nombre del método privado a ejecutar.
|
|
68
|
+
# @param options [Hash] Opciones como `only: [:index]`.
|
|
69
|
+
# @return [void]
|
|
50
70
|
def self.around_action(method_name, **options)
|
|
51
|
-
register_callback(around_actions, method_name, options)
|
|
71
|
+
register_callback(:around_actions, method_name, options)
|
|
52
72
|
end
|
|
53
73
|
|
|
54
|
-
#
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
74
|
+
# Manejo declarativo de excepciones.
|
|
75
|
+
# Atrapa errores específicos que ocurran durante la ejecución de la acción.
|
|
76
|
+
#
|
|
77
|
+
# @example
|
|
78
|
+
# rescue_from Api::Error::NotFound, with: :render_not_found
|
|
79
|
+
# rescue_from StandardError do |e|
|
|
80
|
+
# render status: 500, json: { error: e.message }
|
|
81
|
+
# end
|
|
82
|
+
#
|
|
83
|
+
# @param klasses [Array<Class, String>] Clases de excepciones a atrapar.
|
|
84
|
+
# @param with [Symbol, nil] Nombre del método manejador.
|
|
85
|
+
# @yield [Exception] Bloque opcional para manejar el error inline.
|
|
86
|
+
# @raise [ArgumentError] Si no se provee un manejador (with o block).
|
|
87
|
+
def self.rescue_from(*klasses, with: nil, &block)
|
|
88
|
+
handler = with || block
|
|
89
|
+
raise ArgumentError, "Need a handler. Supply 'with: :method' or a block." unless handler
|
|
90
|
+
|
|
91
|
+
# Duplicamos el array del padre para no mutarlo al registrar reglas en el hijo
|
|
92
|
+
new_handlers = self.rescue_handlers.dup
|
|
93
|
+
|
|
94
|
+
klasses.each do |klass|
|
|
95
|
+
new_handlers.unshift([klass, handler])
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
self.rescue_handlers = new_handlers
|
|
59
99
|
end
|
|
60
100
|
|
|
61
|
-
#
|
|
101
|
+
# Helper interno para registrar callbacks garantizando Thread-Safety e Inmutabilidad del padre.
|
|
102
|
+
# @api private
|
|
103
|
+
def self.register_callback(collection_name, method_name, options)
|
|
104
|
+
current_hash = send(collection_name)
|
|
62
105
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
self.log_tags = []
|
|
106
|
+
# Deep dup: Clonamos el hash y sus arrays internos para no modificar la clase padre
|
|
107
|
+
new_hash = current_hash.transform_values(&:dup)
|
|
66
108
|
|
|
67
|
-
|
|
68
|
-
|
|
109
|
+
only = Array(options[:only]).map(&:to_sym)
|
|
110
|
+
target_actions = only.empty? ? [:_all_actions] : only
|
|
69
111
|
|
|
70
|
-
|
|
112
|
+
target_actions.each do |action|
|
|
113
|
+
new_hash[action] ||= []
|
|
114
|
+
new_hash[action] << method_name
|
|
115
|
+
end
|
|
71
116
|
|
|
72
|
-
|
|
73
|
-
super
|
|
74
|
-
@response_headers = {}
|
|
117
|
+
send("#{collection_name}=", new_hash)
|
|
75
118
|
end
|
|
76
119
|
|
|
77
|
-
#
|
|
120
|
+
# Aplicamos automáticamente las etiquetas de logs a todas las acciones.
|
|
121
|
+
around_action :apply_log_tags
|
|
78
122
|
|
|
79
|
-
# @api private
|
|
80
|
-
def self.rescue_handlers
|
|
81
|
-
@rescue_handlers ||= []
|
|
82
|
-
end
|
|
83
123
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
124
|
+
# ==========================================
|
|
125
|
+
# INICIALIZACIÓN Y CICLO DE VIDA
|
|
126
|
+
# ==========================================
|
|
87
127
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
128
|
+
def initialize(attributes = {})
|
|
129
|
+
super
|
|
130
|
+
@response_headers = {}
|
|
91
131
|
end
|
|
92
132
|
|
|
93
|
-
#
|
|
94
|
-
|
|
133
|
+
# Punto de entrada principal estático llamado por el Router (`BugBunny::Consumer`).
|
|
134
|
+
#
|
|
135
|
+
# @param headers [Hash] Metadatos y variables de enrutamiento.
|
|
136
|
+
# @param body [String, Hash] El payload del mensaje AMQP.
|
|
137
|
+
# @return [Hash] Respuesta final estructurada.
|
|
95
138
|
def self.call(headers:, body: {})
|
|
96
139
|
new(headers: headers).process(body)
|
|
97
140
|
end
|
|
98
141
|
|
|
142
|
+
# Ejecuta el ciclo de vida completo de la petición: Params -> Before -> Action -> Rescue.
|
|
143
|
+
#
|
|
144
|
+
# @param body [String, Hash] El cuerpo del mensaje.
|
|
145
|
+
# @return [Hash] La respuesta lista para ser enviada vía RabbitMQ RPC.
|
|
99
146
|
def process(body)
|
|
100
147
|
prepare_params(body)
|
|
101
148
|
|
|
102
|
-
# Inyección de configuración global de logs si no
|
|
149
|
+
# Inyección de configuración global de logs si el controlador no define propios
|
|
103
150
|
if self.class.log_tags.empty? && BugBunny.configuration.log_tags.any?
|
|
104
151
|
self.class.log_tags = BugBunny.configuration.log_tags
|
|
105
152
|
end
|
|
@@ -118,15 +165,14 @@ module BugBunny
|
|
|
118
165
|
end
|
|
119
166
|
end
|
|
120
167
|
|
|
121
|
-
# Construir la cadena de responsabilidad
|
|
168
|
+
# Construir e invocar la cadena de responsabilidad (Middlewares/Around Actions)
|
|
122
169
|
execution_chain = current_arounds.reverse.inject(core_execution) do |next_step, method_name|
|
|
123
170
|
lambda { send(method_name, &next_step) }
|
|
124
171
|
end
|
|
125
172
|
|
|
126
|
-
# Ejecutar la cadena
|
|
127
173
|
execution_chain.call
|
|
128
174
|
|
|
129
|
-
#
|
|
175
|
+
# Si no hubo renderización explícita, devuelve 204 No Content
|
|
130
176
|
rendered_response || { status: 204, headers: response_headers, body: nil }
|
|
131
177
|
|
|
132
178
|
rescue StandardError => e
|
|
@@ -135,21 +181,14 @@ module BugBunny
|
|
|
135
181
|
|
|
136
182
|
private
|
|
137
183
|
|
|
138
|
-
#
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
(collection[:_all_actions] || []) + (collection[action_name] || [])
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
def run_before_actions(action_name)
|
|
145
|
-
current_befores = resolve_callbacks(self.class.before_actions, action_name)
|
|
146
|
-
current_befores.uniq.each do |method_name|
|
|
147
|
-
send(method_name)
|
|
148
|
-
return false if rendered_response
|
|
149
|
-
end
|
|
150
|
-
true
|
|
151
|
-
end
|
|
184
|
+
# ==========================================
|
|
185
|
+
# HELPERS INTERNOS
|
|
186
|
+
# ==========================================
|
|
152
187
|
|
|
188
|
+
# Evalúa la excepción lanzada y busca el manejador más adecuado definido en `rescue_from`.
|
|
189
|
+
#
|
|
190
|
+
# @param exception [StandardError] La excepción atrapada.
|
|
191
|
+
# @return [Hash] Respuesta de error renderizada.
|
|
153
192
|
def handle_exception(exception)
|
|
154
193
|
handler_entry = self.class.rescue_handlers.find do |klass, _|
|
|
155
194
|
if klass.is_a?(String)
|
|
@@ -161,24 +200,34 @@ module BugBunny
|
|
|
161
200
|
|
|
162
201
|
if handler_entry
|
|
163
202
|
_, handler = handler_entry
|
|
164
|
-
if handler.is_a?(Symbol)
|
|
165
|
-
|
|
203
|
+
if handler.is_a?(Symbol)
|
|
204
|
+
send(handler, exception)
|
|
205
|
+
elsif handler.respond_to?(:call)
|
|
206
|
+
instance_exec(exception, &handler)
|
|
166
207
|
end
|
|
167
208
|
return rendered_response if rendered_response
|
|
168
209
|
end
|
|
169
210
|
|
|
170
|
-
|
|
171
|
-
BugBunny.configuration.logger.error(exception.
|
|
211
|
+
# Fallback genérico si la excepción no fue mapeada
|
|
212
|
+
BugBunny.configuration.logger.error("[BugBunny::Controller] 💥 Unhandled Exception (#{exception.class}): #{exception.message}")
|
|
213
|
+
BugBunny.configuration.logger.error(exception.backtrace.first(5).join("\n"))
|
|
172
214
|
|
|
173
215
|
{
|
|
174
216
|
status: 500,
|
|
175
217
|
headers: response_headers,
|
|
176
|
-
body: { error: exception.message, type: exception.class.name }
|
|
218
|
+
body: { error: "Internal Server Error", detail: exception.message, type: exception.class.name }
|
|
177
219
|
}
|
|
178
220
|
end
|
|
179
221
|
|
|
222
|
+
# Renderiza una respuesta que será enviada de vuelta por la cola reply-to.
|
|
223
|
+
#
|
|
224
|
+
# @param status [Symbol, Integer] Código HTTP (ej. :ok, :not_found, 201).
|
|
225
|
+
# @param json [Object] El payload a serializar como JSON.
|
|
226
|
+
# @return [Hash] La estructura renderizada interna.
|
|
180
227
|
def render(status:, json: nil)
|
|
181
|
-
code = Rack::Utils::SYMBOL_TO_STATUS_CODE[status] ||
|
|
228
|
+
code = Rack::Utils::SYMBOL_TO_STATUS_CODE[status] || status.to_i
|
|
229
|
+
code = 200 if code.zero? # Fallback de seguridad
|
|
230
|
+
|
|
182
231
|
@rendered_response = {
|
|
183
232
|
status: code,
|
|
184
233
|
headers: response_headers,
|
|
@@ -186,21 +235,43 @@ module BugBunny
|
|
|
186
235
|
}
|
|
187
236
|
end
|
|
188
237
|
|
|
238
|
+
# Unifica el query string, parámetros de ruta y el body JSON en un solo objeto `params`.
|
|
189
239
|
def prepare_params(body)
|
|
190
240
|
self.params = {}.with_indifferent_access
|
|
241
|
+
|
|
191
242
|
params.merge!(headers[:query_params]) if headers[:query_params].present?
|
|
192
243
|
params[:id] = headers[:id] if headers[:id].present?
|
|
193
244
|
|
|
194
245
|
if body.is_a?(Hash)
|
|
195
246
|
params.merge!(body)
|
|
196
247
|
elsif body.is_a?(String) && headers[:content_type].to_s.include?('json')
|
|
197
|
-
parsed =
|
|
248
|
+
parsed = begin
|
|
249
|
+
JSON.parse(body)
|
|
250
|
+
rescue JSON::ParserError
|
|
251
|
+
nil
|
|
252
|
+
end
|
|
198
253
|
params.merge!(parsed) if parsed
|
|
199
254
|
else
|
|
200
255
|
self.raw_string = body
|
|
201
256
|
end
|
|
202
257
|
end
|
|
203
258
|
|
|
259
|
+
# Obtiene la lista combinada de callbacks globales y específicos para una acción.
|
|
260
|
+
def resolve_callbacks(collection, action_name)
|
|
261
|
+
(collection[:_all_actions] || []) + (collection[action_name] || [])
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Ejecuta secuencialmente todos los before_actions.
|
|
265
|
+
# Si alguno invoca render(), detiene el flujo devolviendo `false`.
|
|
266
|
+
def run_before_actions(action_name)
|
|
267
|
+
current_befores = resolve_callbacks(self.class.before_actions, action_name)
|
|
268
|
+
current_befores.uniq.each do |method_name|
|
|
269
|
+
send(method_name)
|
|
270
|
+
return false if rendered_response
|
|
271
|
+
end
|
|
272
|
+
true
|
|
273
|
+
end
|
|
274
|
+
|
|
204
275
|
# --- LÓGICA DE LOGGING ENCAPSULADA ---
|
|
205
276
|
|
|
206
277
|
def apply_log_tags
|
|
@@ -225,6 +296,7 @@ module BugBunny
|
|
|
225
296
|
end.compact
|
|
226
297
|
end
|
|
227
298
|
|
|
299
|
+
# @return [String] Identificador único de trazabilidad de la petición.
|
|
228
300
|
def uuid
|
|
229
301
|
headers[:correlation_id] || headers['X-Request-Id']
|
|
230
302
|
end
|