bug_bunny 3.1.1 → 3.1.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 +25 -0
- data/README.md +45 -1
- data/lib/bug_bunny/consumer.rb +15 -3
- data/lib/bug_bunny/controller.rb +141 -69
- data/lib/bug_bunny/exception.rb +45 -21
- data/lib/bug_bunny/middleware/raise_error.rb +68 -18
- data/lib/bug_bunny/producer.rb +11 -1
- data/lib/bug_bunny/resource.rb +34 -26
- data/lib/bug_bunny/version.rb +1 -1
- data/test/test_helper.rb +12 -12
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4aab7949b09bfbf861d4a9ff736bc3bef832fdb3c8a302924544783bdbc9e1bf
|
|
4
|
+
data.tar.gz: 12b73641a812ec4ce72d2a5ad8992a1f2b91c4b0b11ce181aac02fe3a4912751
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2f4475f1754c1de91be6576d4fabb23d025b1f3735d4d76d3123415e71b907d2bfc307dd8e8f4a921ad5a11b2524cd85a1ee17ea3fb1d42f6e15c62ddef01425
|
|
7
|
+
data.tar.gz: c3973288d2d394121491ffb3c7fa809b8841c9fdc3fc87486fb807b62b6f948e48298fd9f4b696132c9b86d448dad82bd4cfa909c39384efc2ab1b7729c15f31
|
data/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
|
+
## [3.1.3] - 2026-02-19
|
|
3
|
+
|
|
4
|
+
### 🏗️ Architectural Refactoring (Middleware Standardization)
|
|
5
|
+
* **Centralized Error Handling:** Refactored `BugBunny::Resource` to completely delegate HTTP status evaluation to the `RaiseError` middleware. The ORM now operates strictly on a "Happy Path" mentality, rescuing semantic exceptions (`NotFound`, `UnprocessableEntity`) natively.
|
|
6
|
+
* **Middleware Injection Enforcement:** `BugBunny::Resource` now explicitly guarantees that `BugBunny::Middleware::RaiseError` and `BugBunny::Middleware::JsonResponse` are the core of the stack, ensuring consistent data parsing and error raising before any custom user middlewares are executed.
|
|
7
|
+
|
|
8
|
+
### ✨ New Features & Improvements
|
|
9
|
+
* **Smart Validation Errors:** `BugBunny::UnprocessableEntity` (422) is now intelligent. It automatically parses the remote worker's response payload, gracefully handling string fallbacks or extracting the standard Rails `{ errors: ... }` convention to accurately populate local object validations.
|
|
10
|
+
* **HTTP 409 Conflict Support:** Added native support for `409 Conflict` mapping it to the new `BugBunny::Conflict` exception. Ideal for handling state collisions in distributed systems.
|
|
11
|
+
* **Global Error Formatting:** Moved `format_error_message` directly into the `RaiseError` middleware. Now, even manual `BugBunny::Client` requests will benefit from clean, structured exception messages (e.g., `"Internal Server Error - undefined method"`) optimized for APMs like Sentry or Datadog.
|
|
12
|
+
|
|
13
|
+
## [3.1.2] - 2026-02-19
|
|
14
|
+
|
|
15
|
+
### 🐛 Bug Fixes
|
|
16
|
+
* **Controller Callback Inheritance:** Fixed a critical issue where `before_action`, `around_action`, and `rescue_from` definitions in a parent class (like `ApplicationController`) were not being inherited by child controllers. Migrated internal storage to `class_attribute` with deep duplication to ensure isolated, thread-safe inheritance without mutating the parent class.
|
|
17
|
+
* **Gemspec Hygiene:** Updated the `spec.description` to resolve RubyGems identity warnings and added explicit minimum version boundaries for standard dependencies (e.g., `json >= 2.0`).
|
|
18
|
+
|
|
19
|
+
### 🌟 Observability & DX (Developer Experience)
|
|
20
|
+
* **Structured Remote Errors:** `BugBunny::Resource` now intelligently formats the body of remote errors. When raising a `ClientError` (4xx) or `InternalServerError` (500), it extracts the specific error message (e.g., `"Internal Server Error - undefined method 'foo'"`) or falls back to a readable JSON string. This drastically improves the legibility of remote stack traces in monitoring tools like Sentry or Datadog.
|
|
21
|
+
* **Infrastructure Logging:** The `Consumer` and `Producer` now calculate the final resolved cascade options (`exchange_opts`, `queue_opts`) and explicitly log them during worker startup and message publishing. This provides absolute transparency into what configurations are actually reaching RabbitMQ.
|
|
22
|
+
* **Consumer Cascade Options:** Added the `exchange_opts:` parameter to `Consumer.subscribe` to fully support Level 3 (On-the-fly) infrastructure configuration for manual worker instantiations.
|
|
23
|
+
|
|
24
|
+
### 📖 Documentation
|
|
25
|
+
* **Built-in Middlewares:** Added comprehensive documentation to the README explaining how to inject and utilize the provided `RaiseError` and `JsonResponse` middlewares when using the manual `BugBunny::Client`.
|
|
26
|
+
|
|
2
27
|
## [3.1.1] - 2026-02-19
|
|
3
28
|
|
|
4
29
|
### 🚀 Features
|
data/README.md
CHANGED
|
@@ -185,7 +185,27 @@ Manager::Service.with(
|
|
|
185
185
|
```
|
|
186
186
|
|
|
187
187
|
### 4. Client Middleware (Interceptores)
|
|
188
|
-
Intercepta peticiones
|
|
188
|
+
Intercepta peticiones de ida y respuestas de vuelta en la arquitectura del cliente.
|
|
189
|
+
|
|
190
|
+
**Middlewares Incluidos (Built-ins)**
|
|
191
|
+
Si usas `BugBunny::Resource`, el manejo de JSON y de errores ya está integrado automáticamente. Pero si utilizas el cliente manual (`BugBunny::Client`), puedes inyectar los middlewares incluidos para no tener que parsear respuestas manualmente:
|
|
192
|
+
|
|
193
|
+
* `BugBunny::Middleware::JsonResponse`: Parsea automáticamente el cuerpo de la respuesta de JSON a un Hash de Ruby.
|
|
194
|
+
* `BugBunny::Middleware::RaiseError`: Evalúa el código de estado (`status`) de la respuesta y lanza excepciones nativas (`BugBunny::NotFound`, `BugBunny::UnprocessableEntity`, `BugBunny::InternalServerError`, etc.).
|
|
195
|
+
|
|
196
|
+
```ruby
|
|
197
|
+
# Uso con el cliente manual
|
|
198
|
+
client = BugBunny::Client.new(pool: BUG_BUNNY_POOL) do |stack|
|
|
199
|
+
stack.use BugBunny::Middleware::RaiseError
|
|
200
|
+
stack.use BugBunny::Middleware::JsonResponse
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Ahora el cliente devolverá Hashes y lanzará errores si el worker falla
|
|
204
|
+
response = client.request('users/1', method: :get)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**Middlewares Personalizados**
|
|
208
|
+
Ideales para inyectar Auth o Headers de trazabilidad en todos los requests de un Recurso.
|
|
189
209
|
|
|
190
210
|
```ruby
|
|
191
211
|
class Manager::Service < BugBunny::Resource
|
|
@@ -200,6 +220,30 @@ class Manager::Service < BugBunny::Resource
|
|
|
200
220
|
end
|
|
201
221
|
```
|
|
202
222
|
|
|
223
|
+
**Personalización Avanzada de Errores**
|
|
224
|
+
Si en tu aplicación necesitas mapear códigos HTTP de negocio (ej. `402 Payment Required`) a excepciones personalizadas, la forma más limpia es usar `Module#prepend` sobre el middleware nativo en un inicializador. De esta forma inyectas tus reglas sin perder el comportamiento por defecto para los demás errores:
|
|
225
|
+
|
|
226
|
+
```ruby
|
|
227
|
+
# config/initializers/bug_bunny_custom_errors.rb
|
|
228
|
+
module CustomBugBunnyErrors
|
|
229
|
+
def on_complete(response)
|
|
230
|
+
status = response['status'].to_i
|
|
231
|
+
|
|
232
|
+
# 1. Reglas específicas de tu negocio
|
|
233
|
+
if status == 402
|
|
234
|
+
raise MyApp::PaymentRequiredError, response['body']['message']
|
|
235
|
+
elsif status == 403 && response['body']['reason'] == 'ip_blocked'
|
|
236
|
+
raise MyApp::IpBlockedError, response['body']['detail']
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# 2. Delegar el resto de los errores (404, 422, 500) al middleware original
|
|
240
|
+
super(response)
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
BugBunny::Middleware::RaiseError.prepend(CustomBugBunnyErrors)
|
|
245
|
+
```
|
|
246
|
+
|
|
203
247
|
---
|
|
204
248
|
|
|
205
249
|
## 📡 Modo Servidor: Controladores
|
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|
|
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
|
|
|
211
|
+
# Fallback genérico si la excepción no fue mapeada
|
|
170
212
|
BugBunny.configuration.logger.error("[BugBunny::Controller] 💥 Unhandled Exception (#{exception.class}): #{exception.message}")
|
|
171
|
-
BugBunny.configuration.logger.error(exception.backtrace.first(5).join("\n"))
|
|
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
|
data/lib/bug_bunny/exception.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
#
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
require 'json'
|
|
3
4
|
|
|
4
5
|
module BugBunny
|
|
@@ -11,10 +12,12 @@ module BugBunny
|
|
|
11
12
|
class CommunicationError < Error; end
|
|
12
13
|
|
|
13
14
|
# Error lanzado cuando ocurren un acceso no permitido a controladores.
|
|
14
|
-
#
|
|
15
|
+
# Protege contra vulnerabilidades de RCE validando la herencia de las clases enrutadas.
|
|
15
16
|
class SecurityError < Error; end
|
|
16
17
|
|
|
17
|
-
#
|
|
18
|
+
# ==========================================
|
|
19
|
+
# Categoría: Errores del Cliente (4xx)
|
|
20
|
+
# ==========================================
|
|
18
21
|
|
|
19
22
|
# Clase base para errores causados por una petición incorrecta del cliente.
|
|
20
23
|
# Corresponde a códigos de estado 400-499.
|
|
@@ -36,7 +39,13 @@ module BugBunny
|
|
|
36
39
|
# El servidor tardó demasiado en responder o el cliente agotó su tiempo de espera (RPC timeout).
|
|
37
40
|
class RequestTimeout < ClientError; end
|
|
38
41
|
|
|
39
|
-
#
|
|
42
|
+
# Error 409: Conflict.
|
|
43
|
+
# implica que la petición es técnicamente válida, pero choca con reglas de negocio o datos existentes
|
|
44
|
+
class Conflict < ClientError; end
|
|
45
|
+
|
|
46
|
+
# ==========================================
|
|
47
|
+
# Categoría: Errores del Servidor (5xx)
|
|
48
|
+
# ==========================================
|
|
40
49
|
|
|
41
50
|
# Clase base para errores causados por fallos en el servidor remoto.
|
|
42
51
|
# Corresponde a códigos de estado 500-599.
|
|
@@ -46,44 +55,59 @@ module BugBunny
|
|
|
46
55
|
# Ocurrió un error inesperado en el worker/servidor remoto al procesar el mensaje.
|
|
47
56
|
class InternalServerError < ServerError; end
|
|
48
57
|
|
|
49
|
-
#
|
|
58
|
+
# ==========================================
|
|
59
|
+
# Categoría: Errores de Validación (422)
|
|
60
|
+
# ==========================================
|
|
50
61
|
|
|
51
62
|
# Error 422: Unprocessable Entity.
|
|
52
63
|
# Indica que la solicitud fue bien formada pero contenía errores semánticos,
|
|
53
64
|
# típicamente fallos de validación en el modelo remoto (ActiveRecord).
|
|
54
65
|
#
|
|
55
|
-
# Esta excepción es
|
|
56
|
-
# para exponer los mensajes de error de forma estructurada
|
|
66
|
+
# Esta excepción es "inteligente": intenta parsear automáticamente el cuerpo
|
|
67
|
+
# de la respuesta para extraer y exponer los mensajes de error de forma estructurada,
|
|
68
|
+
# buscando por convención la clave `errors`.
|
|
57
69
|
class UnprocessableEntity < ClientError
|
|
58
|
-
# @return [Hash, Array] Los mensajes de error
|
|
70
|
+
# @return [Hash, Array, String] Los mensajes de error listos para ser iterados.
|
|
59
71
|
attr_reader :error_messages
|
|
60
72
|
|
|
61
|
-
# @return [String] El cuerpo crudo de la respuesta original.
|
|
73
|
+
# @return [String, Hash] El cuerpo crudo de la respuesta original.
|
|
62
74
|
attr_reader :raw_response
|
|
63
75
|
|
|
64
76
|
# Inicializa la excepción procesando el cuerpo de la respuesta.
|
|
65
77
|
#
|
|
66
|
-
# @param response_body [String, Hash] El cuerpo de la respuesta fallida.
|
|
78
|
+
# @param response_body [String, Hash] El cuerpo de la respuesta fallida (ej. `{ "errors": { "name": ["blank"] } }`).
|
|
67
79
|
def initialize(response_body)
|
|
68
80
|
@raw_response = response_body
|
|
69
|
-
@error_messages =
|
|
81
|
+
@error_messages = extract_errors(response_body)
|
|
70
82
|
super('Validation failed on remote service')
|
|
71
83
|
end
|
|
72
84
|
|
|
73
85
|
private
|
|
74
86
|
|
|
75
|
-
# Intenta convertir el cuerpo de la respuesta a una estructura Ruby
|
|
76
|
-
# Si el cuerpo no es JSON
|
|
87
|
+
# Intenta convertir el cuerpo de la respuesta a una estructura Ruby y extrae la clave 'errors'.
|
|
88
|
+
# Si el cuerpo no sigue la convención o no es JSON, hace un graceful fallback devolviendo
|
|
89
|
+
# el payload completo.
|
|
77
90
|
#
|
|
78
|
-
# @param body [String, Hash] El cuerpo a
|
|
79
|
-
# @return [Object]
|
|
91
|
+
# @param body [String, Hash] El cuerpo a procesar.
|
|
92
|
+
# @return [Object] Los errores aislados o el cuerpo original.
|
|
80
93
|
# @api private
|
|
81
|
-
def
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
94
|
+
def extract_errors(body)
|
|
95
|
+
parsed = if body.is_a?(String)
|
|
96
|
+
begin
|
|
97
|
+
JSON.parse(body)
|
|
98
|
+
rescue JSON::ParserError
|
|
99
|
+
body # Si no es JSON, devolvemos el string tal cual
|
|
100
|
+
end
|
|
101
|
+
else
|
|
102
|
+
body
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
if parsed.is_a?(Hash)
|
|
106
|
+
# Extraemos inteligentemente la clave 'errors' si existe (convención típica de Rails)
|
|
107
|
+
parsed['errors'] || parsed[:errors] || parsed
|
|
108
|
+
else
|
|
109
|
+
parsed
|
|
110
|
+
end
|
|
87
111
|
end
|
|
88
112
|
end
|
|
89
113
|
end
|
|
@@ -1,22 +1,33 @@
|
|
|
1
|
-
# lib/bug_bunny/middleware/raise_error.rb
|
|
2
1
|
# frozen_string_literal: true
|
|
3
2
|
|
|
4
|
-
require_relative '
|
|
3
|
+
require_relative 'base'
|
|
5
4
|
|
|
6
5
|
module BugBunny
|
|
7
6
|
module Middleware
|
|
8
7
|
# Middleware que inspecciona el status de la respuesta y lanza excepciones
|
|
9
8
|
# si se encuentran errores (4xx o 5xx).
|
|
10
9
|
#
|
|
10
|
+
# Extrae inteligentemente el mensaje de error del cuerpo de la respuesta
|
|
11
|
+
# para que las excepciones tengan trazas claras y legibles, evitando el
|
|
12
|
+
# output crudo de Hashes en Ruby (`{ "error" => ... }`).
|
|
13
|
+
#
|
|
11
14
|
# @see BugBunny::Middleware::Base
|
|
12
15
|
class RaiseError < BugBunny::Middleware::Base
|
|
13
16
|
# Hook de ciclo de vida: Ejecutado después de recibir la respuesta.
|
|
14
17
|
#
|
|
15
|
-
# Verifica el código de estado
|
|
18
|
+
# Verifica el código de estado (status) de la respuesta. Si cae en el rango
|
|
19
|
+
# de éxito (2xx), permite que el flujo continúe. Si es un error, lo formatea
|
|
20
|
+
# y lanza la excepción semántica correspondiente.
|
|
16
21
|
#
|
|
17
22
|
# @param response [Hash] El hash de respuesta conteniendo 'status' y 'body'.
|
|
18
|
-
# @raise [BugBunny::
|
|
19
|
-
# @raise [BugBunny::
|
|
23
|
+
# @raise [BugBunny::BadRequest] Si el status es 400.
|
|
24
|
+
# @raise [BugBunny::NotFound] Si el status es 404.
|
|
25
|
+
# @raise [BugBunny::NotAcceptable] Si el status es 406.
|
|
26
|
+
# @raise [BugBunny::RequestTimeout] Si el status es 408.
|
|
27
|
+
# @raise [BugBunny::Conflict] Si el status es 409.
|
|
28
|
+
# @raise [BugBunny::UnprocessableEntity] Si el status es 422.
|
|
29
|
+
# @raise [BugBunny::InternalServerError] Si el status es 500..599.
|
|
30
|
+
# @raise [BugBunny::ClientError, BugBunny::ServerError] Para códigos no mapeados.
|
|
20
31
|
# @return [void]
|
|
21
32
|
def on_complete(response)
|
|
22
33
|
status = response['status'].to_i
|
|
@@ -24,25 +35,64 @@ module BugBunny
|
|
|
24
35
|
|
|
25
36
|
case status
|
|
26
37
|
when 200..299
|
|
27
|
-
|
|
28
|
-
when 400
|
|
29
|
-
|
|
30
|
-
when
|
|
31
|
-
|
|
32
|
-
when
|
|
33
|
-
|
|
38
|
+
return # Flujo normal (Success)
|
|
39
|
+
when 400
|
|
40
|
+
raise BugBunny::BadRequest, format_error_message(body)
|
|
41
|
+
when 404
|
|
42
|
+
raise BugBunny::NotFound
|
|
43
|
+
when 406
|
|
44
|
+
raise BugBunny::NotAcceptable
|
|
45
|
+
when 408
|
|
46
|
+
raise BugBunny::RequestTimeout
|
|
47
|
+
when 409
|
|
48
|
+
raise BugBunny::Conflict, format_error_message(body)
|
|
49
|
+
when 422
|
|
50
|
+
# Pasamos el body crudo; UnprocessableEntity lo procesará en exception.rb
|
|
51
|
+
raise BugBunny::UnprocessableEntity, body
|
|
52
|
+
when 500..599
|
|
53
|
+
raise BugBunny::InternalServerError, format_error_message(body)
|
|
34
54
|
else
|
|
35
|
-
handle_unknown_error(status)
|
|
55
|
+
handle_unknown_error(status, body)
|
|
36
56
|
end
|
|
37
57
|
end
|
|
38
58
|
|
|
39
59
|
private
|
|
40
60
|
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
|
|
45
|
-
|
|
61
|
+
# Formatea el cuerpo de la respuesta de error para que sea legible en las excepciones.
|
|
62
|
+
#
|
|
63
|
+
# Prioriza la convención `{ "error": "...", "detail": "..." }`. Si la respuesta no
|
|
64
|
+
# sigue esta convención, convierte el Hash completo a un JSON string para mantenerlo legible.
|
|
65
|
+
#
|
|
66
|
+
# @param body [Hash, String, nil] El cuerpo de la respuesta.
|
|
67
|
+
# @return [String] Un mensaje de error limpio y estructurado.
|
|
68
|
+
def format_error_message(body)
|
|
69
|
+
return "Unknown Error" if body.nil? || (body.respond_to?(:empty?) && body.empty?)
|
|
70
|
+
return body if body.is_a?(String)
|
|
71
|
+
|
|
72
|
+
# Si el worker devolvió un JSON con una key 'error' (nuestra convención en Controller)
|
|
73
|
+
if body.is_a?(Hash) && body['error']
|
|
74
|
+
detail = body['detail'] ? " - #{body['detail']}" : ""
|
|
75
|
+
"#{body['error']}#{detail}"
|
|
76
|
+
else
|
|
77
|
+
# Fallback: Convertir todo el Hash a JSON string para que se vea claro en Sentry/Logs
|
|
78
|
+
body.to_json
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Maneja códigos de error genéricos no mapeados explícitamente en el `case`.
|
|
83
|
+
#
|
|
84
|
+
# @param status [Integer] El código de estado HTTP (ej. 418, 502).
|
|
85
|
+
# @param body [Object] El cuerpo crudo de la respuesta.
|
|
86
|
+
# @raise [BugBunny::ServerError] Si es 5xx.
|
|
87
|
+
# @raise [BugBunny::ClientError] Si es 4xx.
|
|
88
|
+
def handle_unknown_error(status, body)
|
|
89
|
+
msg = format_error_message(body)
|
|
90
|
+
|
|
91
|
+
if status >= 500
|
|
92
|
+
raise BugBunny::ServerError, "Server Error (#{status}): #{msg}"
|
|
93
|
+
elsif status >= 400
|
|
94
|
+
raise BugBunny::ClientError, "Client Error (#{status}): #{msg}"
|
|
95
|
+
end
|
|
46
96
|
end
|
|
47
97
|
end
|
|
48
98
|
end
|
data/lib/bug_bunny/producer.rb
CHANGED
|
@@ -91,16 +91,26 @@ module BugBunny
|
|
|
91
91
|
|
|
92
92
|
private
|
|
93
93
|
|
|
94
|
+
# Registra la petición en el log calculando las opciones de infraestructura.
|
|
95
|
+
#
|
|
96
|
+
# @param request [BugBunny::Request] Objeto Request que se está enviando.
|
|
97
|
+
# @param payload [String] El cuerpo del mensaje serializado.
|
|
94
98
|
def log_request(request, payload)
|
|
95
99
|
verb = request.method.to_s.upcase
|
|
96
100
|
target = request.path
|
|
97
101
|
rk = request.final_routing_key
|
|
98
102
|
id = request.correlation_id
|
|
99
103
|
|
|
104
|
+
# 📊 LOGGING DE OBSERVABILIDAD: Calculamos las opciones finales para mostrarlas en consola
|
|
105
|
+
final_x_opts = BugBunny::Session::DEFAULT_EXCHANGE_OPTIONS
|
|
106
|
+
.merge(BugBunny.configuration.exchange_options || {})
|
|
107
|
+
.merge(request.exchange_options || {})
|
|
108
|
+
|
|
100
109
|
# INFO: Resumen de una línea (Traffic)
|
|
101
110
|
BugBunny.configuration.logger.info("[BugBunny::Producer] 📤 #{verb} /#{target} | RK: '#{rk}' | ID: #{id}")
|
|
102
111
|
|
|
103
|
-
# DEBUG: Detalle completo
|
|
112
|
+
# DEBUG: Detalle completo de Infraestructura y Payload
|
|
113
|
+
BugBunny.configuration.logger.debug("[BugBunny::Producer] ⚙️ Exchange Opts: #{final_x_opts}")
|
|
104
114
|
BugBunny.configuration.logger.debug("[BugBunny::Producer] 📦 Payload: #{payload.truncate(300)}") if payload.is_a?(String)
|
|
105
115
|
end
|
|
106
116
|
|
data/lib/bug_bunny/resource.rb
CHANGED
|
@@ -15,7 +15,7 @@ module BugBunny
|
|
|
15
15
|
# 3. **Específico:** Definidos en la clase del recurso o vía `with`.
|
|
16
16
|
#
|
|
17
17
|
# @author Gabriel
|
|
18
|
-
# @since 3.1.
|
|
18
|
+
# @since 3.1.2
|
|
19
19
|
class Resource
|
|
20
20
|
include ActiveModel::API
|
|
21
21
|
include ActiveModel::Attributes
|
|
@@ -100,13 +100,22 @@ module BugBunny
|
|
|
100
100
|
stack
|
|
101
101
|
end
|
|
102
102
|
|
|
103
|
-
# Instancia el cliente inyectando los middlewares
|
|
103
|
+
# Instancia el cliente inyectando los middlewares núcleo y personalizados.
|
|
104
|
+
# Integra automáticamente `RaiseError` y `JsonResponse` para que el ORM trabaje
|
|
105
|
+
# puramente con datos parseados o atrape excepciones sin validar HTTP Status manuales.
|
|
106
|
+
#
|
|
104
107
|
# @return [BugBunny::Client]
|
|
105
108
|
def bug_bunny_client
|
|
106
109
|
pool = connection_pool
|
|
107
110
|
raise BugBunny::Error, "Connection pool missing for #{name}" unless pool
|
|
108
|
-
|
|
109
|
-
|
|
111
|
+
|
|
112
|
+
BugBunny::Client.new(pool: pool) do |stack|
|
|
113
|
+
# 1. Middlewares Core (Siempre presentes para el Resource)
|
|
114
|
+
stack.use BugBunny::Middleware::RaiseError
|
|
115
|
+
stack.use BugBunny::Middleware::JsonResponse
|
|
116
|
+
|
|
117
|
+
# 2. Middlewares Personalizados del Usuario
|
|
118
|
+
resolve_middleware_stack.each { |block| block.call(stack) }
|
|
110
119
|
end
|
|
111
120
|
end
|
|
112
121
|
|
|
@@ -164,6 +173,8 @@ module BugBunny
|
|
|
164
173
|
# @!group Acciones CRUD RESTful
|
|
165
174
|
|
|
166
175
|
# Realiza una búsqueda filtrada (GET).
|
|
176
|
+
# Mapea un posible 404 a un array vacío.
|
|
177
|
+
#
|
|
167
178
|
# @param filters [Hash]
|
|
168
179
|
# @return [Array<BugBunny::Resource>]
|
|
169
180
|
def where(filters = {})
|
|
@@ -188,6 +199,8 @@ module BugBunny
|
|
|
188
199
|
inst.send(:clear_changes_information)
|
|
189
200
|
inst
|
|
190
201
|
end
|
|
202
|
+
rescue BugBunny::NotFound
|
|
203
|
+
[]
|
|
191
204
|
end
|
|
192
205
|
|
|
193
206
|
# Devuelve todos los registros.
|
|
@@ -195,6 +208,8 @@ module BugBunny
|
|
|
195
208
|
def all; where({}); end
|
|
196
209
|
|
|
197
210
|
# Busca un registro por ID (GET).
|
|
211
|
+
# Mapea un 404 (NotFound) devolviendo un objeto nulo.
|
|
212
|
+
#
|
|
198
213
|
# @param id [String, Integer]
|
|
199
214
|
# @return [BugBunny::Resource, nil]
|
|
200
215
|
def find(id)
|
|
@@ -211,12 +226,14 @@ module BugBunny
|
|
|
211
226
|
queue_options: current_queue_options
|
|
212
227
|
)
|
|
213
228
|
|
|
214
|
-
return nil
|
|
215
|
-
|
|
229
|
+
return nil unless response && response['body'].is_a?(Hash)
|
|
230
|
+
|
|
216
231
|
instance = new(response['body'])
|
|
217
232
|
instance.persisted = true
|
|
218
233
|
instance.send(:clear_changes_information)
|
|
219
234
|
instance
|
|
235
|
+
rescue BugBunny::NotFound
|
|
236
|
+
nil
|
|
220
237
|
end
|
|
221
238
|
|
|
222
239
|
# Crea una nueva instancia y la persiste.
|
|
@@ -350,7 +367,9 @@ module BugBunny
|
|
|
350
367
|
# @!group Persistencia
|
|
351
368
|
|
|
352
369
|
# Guarda el recurso en el servidor remoto vía AMQP (POST o PUT).
|
|
353
|
-
#
|
|
370
|
+
# Asume el Happy Path; el middleware se encarga de interceptar y lanzar excepciones.
|
|
371
|
+
#
|
|
372
|
+
# @return [Boolean] Retorna true si tuvo éxito, false si falló la validación.
|
|
354
373
|
def save
|
|
355
374
|
return false unless valid?
|
|
356
375
|
|
|
@@ -364,6 +383,7 @@ module BugBunny
|
|
|
364
383
|
path = is_new ? self.class.resource_name : "#{self.class.resource_name}/#{id}"
|
|
365
384
|
method = is_new ? :post : :put
|
|
366
385
|
|
|
386
|
+
# Si el middleware de errores no lanza excepción, asumimos un éxito (200..299)
|
|
367
387
|
response = bug_bunny_client.request(
|
|
368
388
|
path,
|
|
369
389
|
method: method,
|
|
@@ -375,7 +395,10 @@ module BugBunny
|
|
|
375
395
|
body: wrapped_payload
|
|
376
396
|
)
|
|
377
397
|
|
|
378
|
-
|
|
398
|
+
assign_attributes(response['body'])
|
|
399
|
+
self.persisted = true
|
|
400
|
+
clear_changes_information
|
|
401
|
+
true
|
|
379
402
|
end
|
|
380
403
|
rescue BugBunny::UnprocessableEntity => e
|
|
381
404
|
load_remote_rabbit_errors(e.error_messages)
|
|
@@ -383,6 +406,7 @@ module BugBunny
|
|
|
383
406
|
end
|
|
384
407
|
|
|
385
408
|
# Elimina el recurso del servidor remoto (DELETE).
|
|
409
|
+
#
|
|
386
410
|
# @return [Boolean]
|
|
387
411
|
def destroy
|
|
388
412
|
return false unless persisted?
|
|
@@ -409,25 +433,9 @@ module BugBunny
|
|
|
409
433
|
|
|
410
434
|
private
|
|
411
435
|
|
|
412
|
-
#
|
|
413
|
-
def handle_save_response(response)
|
|
414
|
-
if response['status'] == 422
|
|
415
|
-
raise BugBunny::UnprocessableEntity.new(response['body']['errors'] || response['body'])
|
|
416
|
-
elsif response['status'] >= 500
|
|
417
|
-
raise BugBunny::InternalServerError
|
|
418
|
-
elsif response['status'] >= 400
|
|
419
|
-
raise BugBunny::ClientError
|
|
420
|
-
end
|
|
421
|
-
|
|
422
|
-
assign_attributes(response['body'])
|
|
423
|
-
self.persisted = true
|
|
424
|
-
clear_changes_information
|
|
425
|
-
true
|
|
426
|
-
end
|
|
427
|
-
|
|
428
|
-
# Carga errores remotos en el objeto local.
|
|
436
|
+
# Carga errores remotos en el objeto local (utilizado al recibir 422).
|
|
429
437
|
def load_remote_rabbit_errors(errors_hash)
|
|
430
|
-
return if errors_hash.nil?
|
|
438
|
+
return if errors_hash.nil? || errors_hash.empty?
|
|
431
439
|
if errors_hash.is_a?(String)
|
|
432
440
|
errors.add(:base, errors_hash)
|
|
433
441
|
else
|
data/lib/bug_bunny/version.rb
CHANGED
data/test/test_helper.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'bundler/setup'
|
|
4
4
|
require 'minitest/autorun'
|
|
5
|
-
# require 'minitest/reporters'
|
|
5
|
+
# require 'minitest/reporters'
|
|
6
6
|
require 'bug_bunny'
|
|
7
7
|
require 'connection_pool'
|
|
8
8
|
require 'securerandom'
|
|
@@ -14,7 +14,7 @@ BugBunny.configure do |config|
|
|
|
14
14
|
config.password = ENV.fetch('RABBITMQ_PASS', 'guest')
|
|
15
15
|
config.vhost = '/'
|
|
16
16
|
config.logger = Logger.new($stdout)
|
|
17
|
-
config.logger.level = Logger::WARN
|
|
17
|
+
config.logger.level = Logger::WARN
|
|
18
18
|
|
|
19
19
|
# ========================================================
|
|
20
20
|
# LA MAGIA DE LA CASCADA (Nivel 2: Configuración Global)
|
|
@@ -38,10 +38,10 @@ module IntegrationHelper
|
|
|
38
38
|
|
|
39
39
|
def with_running_worker(queue:, exchange:, exchange_type: 'topic', routing_key: '#')
|
|
40
40
|
conn = BugBunny.create_connection
|
|
41
|
-
|
|
41
|
+
|
|
42
42
|
worker_thread = Thread.new do
|
|
43
43
|
ch = conn.create_channel
|
|
44
|
-
|
|
44
|
+
|
|
45
45
|
x_opts = BugBunny.configuration.exchange_options || {}
|
|
46
46
|
q_opts = BugBunny.configuration.queue_options || {}
|
|
47
47
|
|
|
@@ -63,7 +63,7 @@ module IntegrationHelper
|
|
|
63
63
|
puts e.backtrace.join("\n")
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
-
sleep 0.5
|
|
66
|
+
sleep 0.5
|
|
67
67
|
yield
|
|
68
68
|
ensure
|
|
69
69
|
conn&.close
|
|
@@ -74,26 +74,26 @@ module IntegrationHelper
|
|
|
74
74
|
def with_spy_worker(queue:, exchange:, exchange_type: 'topic', routing_key: '#')
|
|
75
75
|
captured_messages = Thread::Queue.new
|
|
76
76
|
conn = BugBunny.create_connection
|
|
77
|
-
|
|
77
|
+
|
|
78
78
|
worker_thread = Thread.new do
|
|
79
79
|
ch = conn.create_channel
|
|
80
|
-
|
|
80
|
+
|
|
81
81
|
x_opts = BugBunny.configuration.exchange_options || {}
|
|
82
82
|
q_opts = BugBunny.configuration.queue_options || {}
|
|
83
|
-
|
|
83
|
+
|
|
84
84
|
# FIX DEFINITIVO: Ahora 'x' es un hermoso objeto Bunny::Exchange
|
|
85
85
|
# que la función .bind() entiende perfectamente.
|
|
86
86
|
x = ch.public_send(exchange_type, exchange, x_opts)
|
|
87
87
|
q = ch.queue(queue, q_opts)
|
|
88
|
-
|
|
88
|
+
|
|
89
89
|
# Bindeamos el objeto al queue
|
|
90
90
|
q.bind(x, routing_key: routing_key)
|
|
91
91
|
|
|
92
92
|
q.subscribe(block: true) do |delivery, props, body|
|
|
93
|
-
captured_messages << {
|
|
94
|
-
body: body,
|
|
93
|
+
captured_messages << {
|
|
94
|
+
body: body,
|
|
95
95
|
routing_key: delivery.routing_key,
|
|
96
|
-
headers: props.headers
|
|
96
|
+
headers: props.headers
|
|
97
97
|
}
|
|
98
98
|
end
|
|
99
99
|
rescue => e
|