bug_bunny 4.6.1 → 4.8.0
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/.agents/skills/rabbitmq-expert/SKILL.md +1555 -0
- data/.claude/commands/gem-ai-setup.md +174 -0
- data/.claude/commands/pr.md +53 -0
- data/.claude/commands/release.md +52 -0
- data/.claude/commands/rubocop.md +22 -0
- data/.claude/commands/service-ai-setup.md +168 -0
- data/.claude/commands/test.md +28 -0
- data/.claude/commands/yard.md +46 -0
- data/CHANGELOG.md +50 -15
- data/CLAUDE.md +240 -0
- data/README.md +154 -221
- data/Rakefile +19 -3
- data/docs/_index.md +50 -0
- data/docs/ai/_index.md +56 -0
- data/docs/ai/antipatterns.md +166 -0
- data/docs/ai/api.md +251 -0
- data/docs/ai/architecture.md +92 -0
- data/docs/ai/errors.md +158 -0
- data/docs/ai/faq_external.md +133 -0
- data/docs/ai/faq_internal.md +86 -0
- data/docs/ai/glossary.md +45 -0
- data/docs/concepts.md +140 -0
- data/docs/howto/controller.md +194 -0
- data/docs/howto/middleware_client.md +119 -0
- data/docs/howto/middleware_consumer.md +127 -0
- data/docs/howto/rails.md +214 -0
- data/docs/howto/resource.md +200 -0
- data/docs/howto/routing.md +133 -0
- data/docs/howto/testing.md +259 -0
- data/docs/howto/tracing.md +119 -0
- data/lib/bug_bunny/client.rb +45 -21
- data/lib/bug_bunny/configuration.rb +63 -0
- data/lib/bug_bunny/consumer.rb +51 -37
- data/lib/bug_bunny/consumer_middleware.rb +14 -5
- data/lib/bug_bunny/controller.rb +39 -18
- data/lib/bug_bunny/exception.rb +5 -1
- data/lib/bug_bunny/middleware/raise_error.rb +3 -3
- data/lib/bug_bunny/observability.rb +28 -6
- data/lib/bug_bunny/producer.rb +11 -13
- data/lib/bug_bunny/railtie.rb +8 -7
- data/lib/bug_bunny/request.rb +3 -11
- data/lib/bug_bunny/resource.rb +81 -41
- data/lib/bug_bunny/routing/route.rb +6 -1
- data/lib/bug_bunny/routing/route_set.rb +60 -22
- data/lib/bug_bunny/session.rb +18 -11
- data/lib/bug_bunny/version.rb +1 -1
- data/lib/bug_bunny.rb +4 -2
- data/lib/generators/bug_bunny/install/install_generator.rb +45 -5
- data/lib/tasks/bug_bunny.rake +50 -0
- data/plan_test.txt +63 -0
- data/skills-lock.json +10 -0
- data/spec/integration/client_spec.rb +117 -0
- data/spec/integration/consumer_middleware_spec.rb +86 -0
- data/spec/integration/controller_spec.rb +140 -0
- data/spec/integration/error_handling_spec.rb +57 -0
- data/spec/integration/infrastructure_spec.rb +52 -0
- data/spec/integration/resource_spec.rb +113 -0
- data/spec/spec_helper.rb +70 -0
- data/spec/support/bunny_mocks.rb +18 -0
- data/spec/support/integration_helper.rb +87 -0
- data/spec/unit/client_session_pool_spec.rb +159 -0
- data/spec/unit/configuration_spec.rb +164 -0
- data/spec/unit/consumer_middleware_spec.rb +129 -0
- data/spec/unit/consumer_spec.rb +90 -0
- data/spec/unit/controller_after_action_spec.rb +155 -0
- data/spec/unit/observability_spec.rb +167 -0
- data/spec/unit/resource_attributes_spec.rb +69 -0
- data/spec/unit/session_spec.rb +98 -0
- metadata +50 -3
- data/sig/bug_bunny.rbs +0 -4
data/lib/bug_bunny/resource.rb
CHANGED
|
@@ -25,8 +25,7 @@ module BugBunny
|
|
|
25
25
|
define_model_callbacks :save, :create, :update, :destroy
|
|
26
26
|
|
|
27
27
|
attr_reader :remote_attributes
|
|
28
|
-
attr_accessor :persisted
|
|
29
|
-
attr_accessor :routing_key, :exchange, :exchange_type
|
|
28
|
+
attr_accessor :persisted, :routing_key, :exchange, :exchange_type
|
|
30
29
|
|
|
31
30
|
# @return [Hash] Opciones específicas de instancia para exchange y queue.
|
|
32
31
|
attr_accessor :exchange_options, :queue_options
|
|
@@ -38,7 +37,9 @@ module BugBunny
|
|
|
38
37
|
attr_writer :exchange_options, :queue_options
|
|
39
38
|
|
|
40
39
|
# @api private
|
|
41
|
-
def thread_config(key)
|
|
40
|
+
def thread_config(key)
|
|
41
|
+
Thread.current["bb_#{object_id}_#{key}"]
|
|
42
|
+
end
|
|
42
43
|
|
|
43
44
|
# Resuelve la configuración buscando en el hilo, luego en la jerarquía de clases.
|
|
44
45
|
# @param key [Symbol] Clave en el Thread.current.
|
|
@@ -47,29 +48,41 @@ module BugBunny
|
|
|
47
48
|
def resolve_config(key, instance_var)
|
|
48
49
|
val = thread_config(key)
|
|
49
50
|
return val if val
|
|
51
|
+
|
|
50
52
|
target = self
|
|
51
53
|
while target <= BugBunny::Resource
|
|
52
54
|
value = target.instance_variable_get(instance_var)
|
|
53
55
|
return value.respond_to?(:call) ? value.call : value unless value.nil?
|
|
56
|
+
|
|
54
57
|
target = target.superclass
|
|
55
58
|
end
|
|
56
59
|
nil
|
|
57
60
|
end
|
|
58
61
|
|
|
59
62
|
# @return [ConnectionPool, nil]
|
|
60
|
-
def connection_pool
|
|
63
|
+
def connection_pool
|
|
64
|
+
resolve_config(:pool, :@connection_pool)
|
|
65
|
+
end
|
|
61
66
|
|
|
62
67
|
# @return [String] Nombre del exchange actual.
|
|
63
|
-
def current_exchange
|
|
68
|
+
def current_exchange
|
|
69
|
+
resolve_config(:exchange, :@exchange) || raise(ArgumentError, "Exchange not defined for #{name}")
|
|
70
|
+
end
|
|
64
71
|
|
|
65
72
|
# @return [String] Tipo de exchange ('direct', 'topic', 'fanout').
|
|
66
|
-
def current_exchange_type
|
|
73
|
+
def current_exchange_type
|
|
74
|
+
resolve_config(:exchange_type, :@exchange_type) || 'direct'
|
|
75
|
+
end
|
|
67
76
|
|
|
68
77
|
# @return [Hash] Opciones de exchange específicas (Nivel 3 de la cascada).
|
|
69
|
-
def current_exchange_options
|
|
78
|
+
def current_exchange_options
|
|
79
|
+
resolve_config(:exchange_options, :@exchange_options) || {}
|
|
80
|
+
end
|
|
70
81
|
|
|
71
82
|
# @return [Hash] Opciones de cola específicas.
|
|
72
|
-
def current_queue_options
|
|
83
|
+
def current_queue_options
|
|
84
|
+
resolve_config(:queue_options, :@queue_options) || {}
|
|
85
|
+
end
|
|
73
86
|
|
|
74
87
|
# @return [String] Nombre del recurso para la construcción de rutas.
|
|
75
88
|
def resource_name
|
|
@@ -126,7 +139,8 @@ module BugBunny
|
|
|
126
139
|
# @param pool [ConnectionPool] Pool de conexiones.
|
|
127
140
|
# @param exchange_options [Hash] Opciones de infraestructura.
|
|
128
141
|
# @param queue_options [Hash] Opciones de cola.
|
|
129
|
-
def with(exchange: nil, routing_key: nil, exchange_type: nil, pool: nil, exchange_options: nil,
|
|
142
|
+
def with(exchange: nil, routing_key: nil, exchange_type: nil, pool: nil, exchange_options: nil,
|
|
143
|
+
queue_options: nil)
|
|
130
144
|
keys = {
|
|
131
145
|
exchange: "bb_#{object_id}_exchange",
|
|
132
146
|
exchange_type: "bb_#{object_id}_exchange_type",
|
|
@@ -163,9 +177,7 @@ module BugBunny
|
|
|
163
177
|
end
|
|
164
178
|
|
|
165
179
|
def method_missing(method, *args, &block)
|
|
166
|
-
if @used
|
|
167
|
-
::Kernel.raise ::BugBunny::Error, "ScopeProxy is single-use. Call .with again for a new context."
|
|
168
|
-
end
|
|
180
|
+
::Kernel.raise ::BugBunny::Error, 'ScopeProxy is single-use. Call .with again for a new context.' if @used
|
|
169
181
|
@used = true
|
|
170
182
|
@target.public_send(method, *args, &block)
|
|
171
183
|
ensure
|
|
@@ -176,11 +188,13 @@ module BugBunny
|
|
|
176
188
|
# Calcula la routing key final.
|
|
177
189
|
# @param id [String, nil] ID del recurso.
|
|
178
190
|
# @return [String]
|
|
179
|
-
def calculate_routing_key(
|
|
191
|
+
def calculate_routing_key(_id = nil)
|
|
180
192
|
manual_rk = thread_config(:routing_key)
|
|
181
193
|
return manual_rk if manual_rk
|
|
194
|
+
|
|
182
195
|
static_rk = resolve_config(:routing_key, :@routing_key)
|
|
183
196
|
return static_rk if static_rk.present?
|
|
197
|
+
|
|
184
198
|
resource_name
|
|
185
199
|
end
|
|
186
200
|
|
|
@@ -206,6 +220,7 @@ module BugBunny
|
|
|
206
220
|
)
|
|
207
221
|
|
|
208
222
|
return [] unless response['body'].is_a?(Array)
|
|
223
|
+
|
|
209
224
|
response['body'].map do |attrs|
|
|
210
225
|
inst = new(attrs)
|
|
211
226
|
inst.persisted = true
|
|
@@ -218,7 +233,9 @@ module BugBunny
|
|
|
218
233
|
|
|
219
234
|
# Devuelve todos los registros.
|
|
220
235
|
# @return [Array<BugBunny::Resource>]
|
|
221
|
-
def all
|
|
236
|
+
def all
|
|
237
|
+
where({})
|
|
238
|
+
end
|
|
222
239
|
|
|
223
240
|
# Busca un registro por ID (GET).
|
|
224
241
|
# Mapea un 404 (NotFound) devolviendo un objeto nulo.
|
|
@@ -264,8 +281,8 @@ module BugBunny
|
|
|
264
281
|
# Inicializa el recurso.
|
|
265
282
|
# @param attributes [Hash]
|
|
266
283
|
def initialize(attributes = {})
|
|
267
|
-
@
|
|
268
|
-
@dynamic_changes = Set.new
|
|
284
|
+
@extra_attributes = {}.with_indifferent_access
|
|
285
|
+
@dynamic_changes = Set.new
|
|
269
286
|
@persisted = false
|
|
270
287
|
|
|
271
288
|
# Contexto de infraestructura
|
|
@@ -275,41 +292,61 @@ module BugBunny
|
|
|
275
292
|
@exchange_options = self.class.thread_config(:exchange_options) || self.class.current_exchange_options
|
|
276
293
|
@queue_options = self.class.thread_config(:queue_options) || self.class.current_queue_options
|
|
277
294
|
|
|
278
|
-
super
|
|
279
|
-
assign_attributes(attributes)
|
|
295
|
+
super
|
|
280
296
|
end
|
|
281
297
|
|
|
282
|
-
# Limpia
|
|
298
|
+
# Limpia el rastreo de ActiveModel y nuestro rastreo dinámico interno.
|
|
283
299
|
def clear_changes_information
|
|
284
300
|
super
|
|
285
301
|
@dynamic_changes.clear
|
|
286
302
|
end
|
|
287
303
|
|
|
304
|
+
# @return [Boolean] true si hay cambios nativos o dinámicos.
|
|
305
|
+
def changed?
|
|
306
|
+
super || @dynamic_changes.any?
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# @return [Array<String>] Lista de atributos que han cambiado.
|
|
310
|
+
def changed
|
|
311
|
+
(super + @dynamic_changes.to_a).uniq
|
|
312
|
+
end
|
|
313
|
+
|
|
288
314
|
# Serialización combinada.
|
|
289
315
|
# @return [Hash]
|
|
290
316
|
def attributes_for_serialization
|
|
291
|
-
@
|
|
317
|
+
@extra_attributes.merge(attributes)
|
|
292
318
|
end
|
|
293
319
|
|
|
294
320
|
# @return [String]
|
|
295
|
-
def calculate_routing_key(id=nil)
|
|
321
|
+
def calculate_routing_key(id = nil)
|
|
322
|
+
@routing_key || self.class.calculate_routing_key(id)
|
|
323
|
+
end
|
|
296
324
|
|
|
297
325
|
# @return [String]
|
|
298
|
-
def current_exchange
|
|
326
|
+
def current_exchange
|
|
327
|
+
@exchange || self.class.current_exchange
|
|
328
|
+
end
|
|
299
329
|
|
|
300
330
|
# @return [String]
|
|
301
|
-
def current_exchange_type
|
|
331
|
+
def current_exchange_type
|
|
332
|
+
@exchange_type || self.class.current_exchange_type
|
|
333
|
+
end
|
|
302
334
|
|
|
303
335
|
# @return [BugBunny::Client]
|
|
304
|
-
def bug_bunny_client
|
|
336
|
+
def bug_bunny_client
|
|
337
|
+
self.class.bug_bunny_client
|
|
338
|
+
end
|
|
305
339
|
|
|
306
340
|
# @return [Boolean]
|
|
307
|
-
def persisted
|
|
341
|
+
def persisted?
|
|
342
|
+
!!@persisted
|
|
343
|
+
end
|
|
308
344
|
|
|
309
345
|
# Asignación masiva de atributos.
|
|
310
346
|
# @param new_attributes [Hash]
|
|
311
347
|
def assign_attributes(new_attributes)
|
|
312
348
|
return if new_attributes.nil?
|
|
349
|
+
|
|
313
350
|
new_attributes.each { |k, v| public_send("#{k}=", v) }
|
|
314
351
|
end
|
|
315
352
|
|
|
@@ -324,57 +361,58 @@ module BugBunny
|
|
|
324
361
|
# Retorna el hash combinado de cambios (Tipados + Dinámicos).
|
|
325
362
|
# @return [Hash]
|
|
326
363
|
def changes_to_send
|
|
327
|
-
# 1.
|
|
328
|
-
|
|
364
|
+
# 1. Obtener los nombres de todos los atributos que han cambiado (incluyendo dinámicos vía attribute_will_change!)
|
|
365
|
+
changed_keys = changed
|
|
329
366
|
|
|
330
|
-
# 2.
|
|
331
|
-
|
|
332
|
-
|
|
367
|
+
# 2. Construir el payload con los valores actuales de esas keys
|
|
368
|
+
payload = {}
|
|
369
|
+
changed_keys.each do |key|
|
|
370
|
+
payload[key] = public_send(key)
|
|
333
371
|
end
|
|
334
372
|
|
|
335
373
|
return payload unless payload.empty?
|
|
336
374
|
|
|
337
|
-
# Fallback: Si no hay cambios detectados
|
|
375
|
+
# Fallback: Si no hay cambios detectados (ej: en un create), enviamos todo
|
|
338
376
|
attributes_for_serialization.except('id', 'ID', 'Id', '_id')
|
|
339
377
|
end
|
|
340
378
|
|
|
341
|
-
# Intercepta asignaciones dinámicas y las
|
|
379
|
+
# Intercepta asignaciones dinámicas y las registra como cambios.
|
|
342
380
|
def method_missing(method_name, *args, &block)
|
|
343
381
|
attribute_name = method_name.to_s
|
|
344
382
|
if attribute_name.end_with?('=')
|
|
345
383
|
key = attribute_name.chop
|
|
346
384
|
val = args.first
|
|
347
385
|
|
|
348
|
-
if @
|
|
386
|
+
if @extra_attributes[key] != val
|
|
349
387
|
@dynamic_changes << key
|
|
388
|
+
@extra_attributes[key] = val
|
|
350
389
|
end
|
|
351
|
-
|
|
352
|
-
@remote_attributes[key] = val
|
|
353
390
|
else
|
|
354
|
-
@
|
|
391
|
+
@extra_attributes.key?(attribute_name) ? @extra_attributes[attribute_name] : super
|
|
355
392
|
end
|
|
356
393
|
end
|
|
357
394
|
|
|
358
395
|
def respond_to_missing?(method_name, include_private = false)
|
|
359
|
-
@
|
|
396
|
+
@extra_attributes.key?(method_name.to_s.sub(/=$/, '')) || super
|
|
360
397
|
end
|
|
361
398
|
|
|
362
399
|
# @return [Object] Valor del ID buscando en múltiples nomenclaturas.
|
|
363
400
|
def id
|
|
364
|
-
attributes['id'] || @
|
|
401
|
+
attributes['id'] || @extra_attributes['id'] || @extra_attributes['ID'] || @extra_attributes['Id'] || @extra_attributes['_id']
|
|
365
402
|
end
|
|
366
403
|
|
|
367
404
|
def id=(value)
|
|
368
405
|
if self.class.attribute_names.include?('id')
|
|
369
|
-
super
|
|
406
|
+
super
|
|
370
407
|
else
|
|
371
|
-
@
|
|
408
|
+
@dynamic_changes << 'id' if @extra_attributes['id'] != value
|
|
409
|
+
@extra_attributes['id'] = value
|
|
372
410
|
end
|
|
373
411
|
end
|
|
374
412
|
|
|
375
413
|
def read_attribute_for_validation(attr)
|
|
376
414
|
attr_s = attr.to_s
|
|
377
|
-
self.class.attribute_names.include?(attr_s) ? attribute(attr_s) : @
|
|
415
|
+
self.class.attribute_names.include?(attr_s) ? attribute(attr_s) : @extra_attributes[attr_s]
|
|
378
416
|
end
|
|
379
417
|
|
|
380
418
|
# @!group Persistencia
|
|
@@ -423,6 +461,7 @@ module BugBunny
|
|
|
423
461
|
# @return [Boolean]
|
|
424
462
|
def destroy
|
|
425
463
|
return false unless persisted?
|
|
464
|
+
|
|
426
465
|
run_callbacks(:destroy) do
|
|
427
466
|
path = "#{self.class.resource_name}/#{id}"
|
|
428
467
|
rk = calculate_routing_key(id)
|
|
@@ -449,6 +488,7 @@ module BugBunny
|
|
|
449
488
|
# Carga errores remotos en el objeto local (utilizado al recibir 422).
|
|
450
489
|
def load_remote_rabbit_errors(errors_hash)
|
|
451
490
|
return if errors_hash.nil? || errors_hash.empty?
|
|
491
|
+
|
|
452
492
|
if errors_hash.is_a?(String)
|
|
453
493
|
errors.add(:base, errors_hash)
|
|
454
494
|
else
|
|
@@ -17,6 +17,9 @@ module BugBunny
|
|
|
17
17
|
# @return [String] El nombre del controlador en formato snake_case (ej. 'api/v1/metrics').
|
|
18
18
|
attr_reader :controller
|
|
19
19
|
|
|
20
|
+
# @return [String, nil] El namespace del controlador si existe (ej. 'Api::V1').
|
|
21
|
+
attr_reader :namespace
|
|
22
|
+
|
|
20
23
|
# @return [String] El nombre de la acción a ejecutar (ej. 'show').
|
|
21
24
|
attr_reader :action
|
|
22
25
|
|
|
@@ -25,10 +28,12 @@ module BugBunny
|
|
|
25
28
|
# @param http_method [String, Symbol] Verbo HTTP (ej. :get, 'POST').
|
|
26
29
|
# @param path_pattern [String] Patrón de la URL. Los parámetros dinámicos deben iniciar con ':' (ej. 'users/:id').
|
|
27
30
|
# @param to [String] Destino en formato 'controlador#accion' (ej. 'users#show').
|
|
31
|
+
# @param namespace [String, nil] El namespace del controlador (ej: 'Api::V1').
|
|
28
32
|
# @raise [ArgumentError] Si el formato del destino `to` es inválido.
|
|
29
|
-
def initialize(http_method, path_pattern, to:)
|
|
33
|
+
def initialize(http_method, path_pattern, to:, namespace: nil)
|
|
30
34
|
@http_method = http_method.to_s.upcase
|
|
31
35
|
@path_pattern = normalize_path(path_pattern)
|
|
36
|
+
@namespace = namespace
|
|
32
37
|
|
|
33
38
|
parse_destination!(to)
|
|
34
39
|
compile_regex!
|
|
@@ -57,30 +57,57 @@ module BugBunny
|
|
|
57
57
|
# Registra una ruta para el verbo GET.
|
|
58
58
|
# @param path [String, Symbol] Patrón de la URL.
|
|
59
59
|
# @param to [String, nil] Destino (controlador#accion). Si es nil, se infiere del scope.
|
|
60
|
-
def get(path, to: nil)
|
|
60
|
+
def get(path, to: nil)
|
|
61
|
+
add_route('GET', path, to: to)
|
|
62
|
+
end
|
|
61
63
|
|
|
62
64
|
# Registra una ruta para el verbo POST.
|
|
63
65
|
# @param path [String, Symbol] Patrón de la URL.
|
|
64
66
|
# @param to [String, nil] Destino (controlador#accion). Si es nil, se infiere del scope.
|
|
65
|
-
def post(path, to: nil)
|
|
67
|
+
def post(path, to: nil)
|
|
68
|
+
add_route('POST', path, to: to)
|
|
69
|
+
end
|
|
66
70
|
|
|
67
71
|
# Registra una ruta para el verbo PUT.
|
|
68
72
|
# @param path [String, Symbol] Patrón de la URL.
|
|
69
73
|
# @param to [String, nil] Destino (controlador#accion). Si es nil, se infiere del scope.
|
|
70
|
-
def put(path, to: nil)
|
|
74
|
+
def put(path, to: nil)
|
|
75
|
+
add_route('PUT', path, to: to)
|
|
76
|
+
end
|
|
71
77
|
|
|
72
78
|
# Registra una ruta para el verbo PATCH.
|
|
73
79
|
# @param path [String, Symbol] Patrón de la URL.
|
|
74
80
|
# @param to [String, nil] Destino (controlador#accion). Si es nil, se infiere del scope.
|
|
75
|
-
def patch(path, to: nil)
|
|
81
|
+
def patch(path, to: nil)
|
|
82
|
+
add_route('PATCH', path, to: to)
|
|
83
|
+
end
|
|
76
84
|
|
|
77
85
|
# Registra una ruta para el verbo DELETE.
|
|
78
86
|
# @param path [String, Symbol] Patrón de la URL.
|
|
79
87
|
# @param to [String, nil] Destino (controlador#accion). Si es nil, se infiere del scope.
|
|
80
|
-
def delete(path, to: nil)
|
|
88
|
+
def delete(path, to: nil)
|
|
89
|
+
add_route('DELETE', path, to: to)
|
|
90
|
+
end
|
|
81
91
|
|
|
82
92
|
# @!endgroup
|
|
83
93
|
|
|
94
|
+
# Define un bloque de namespace para organizar controladores en módulos.
|
|
95
|
+
# Los namespaces pueden anidarse y se acumulan (ej: `namespace :api { namespace :v1 }`
|
|
96
|
+
# resulta en el namespace "Api::V1").
|
|
97
|
+
#
|
|
98
|
+
# @param name [Symbol, String] Nombre del namespace (ej: :api, :v1).
|
|
99
|
+
# @yield Bloque conteniendo las definiciones de rutas dentro de este namespace.
|
|
100
|
+
# @return [void]
|
|
101
|
+
# @example
|
|
102
|
+
# namespace :api do
|
|
103
|
+
# resources :users # Busca Api::UsersController
|
|
104
|
+
# end
|
|
105
|
+
def namespace(name, &block)
|
|
106
|
+
with_scope({ type: :namespace, name: name.to_s.camelize }) do
|
|
107
|
+
instance_eval(&block)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
84
111
|
# Macro que genera automáticamente las rutas CRUD para un recurso RESTful.
|
|
85
112
|
# Soporta filtrado mediante `only` y `except`, y acepta un bloque para rutas anidadas.
|
|
86
113
|
#
|
|
@@ -96,7 +123,7 @@ module BugBunny
|
|
|
96
123
|
resource_path = name.to_s
|
|
97
124
|
|
|
98
125
|
# Todas las acciones estándar disponibles
|
|
99
|
-
actions = [
|
|
126
|
+
actions = %i[index show create update destroy]
|
|
100
127
|
|
|
101
128
|
# Aplicamos los filtros si existen
|
|
102
129
|
if only
|
|
@@ -114,10 +141,10 @@ module BugBunny
|
|
|
114
141
|
delete "#{resource_path}/:id", to: "#{resource_path}#destroy" if actions.include?(:destroy)
|
|
115
142
|
|
|
116
143
|
# Si se pasa un bloque, abrimos un Scope de Recurso para rutas anidadas
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
144
|
+
return unless block_given?
|
|
145
|
+
|
|
146
|
+
with_scope({ type: :resource, name: resource_path }) do
|
|
147
|
+
instance_eval(&block)
|
|
121
148
|
end
|
|
122
149
|
end
|
|
123
150
|
|
|
@@ -177,18 +204,19 @@ module BugBunny
|
|
|
177
204
|
#
|
|
178
205
|
# @param method [String] Verbo HTTP entrante (ej. 'GET').
|
|
179
206
|
# @param path [String] URL entrante (ej. 'nodes/123/drain').
|
|
180
|
-
# @return [Hash, nil] Hash con `:controller`, `:action` y `:
|
|
207
|
+
# @return [Hash, nil] Hash con `:controller`, `:action`, `:params` y `:namespace`, o `nil` si no hay match.
|
|
181
208
|
def recognize(method, path)
|
|
182
209
|
@routes.each do |route|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
210
|
+
next unless route.match?(method, path)
|
|
211
|
+
|
|
212
|
+
extracted_params = route.extract_params(path)
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
controller: route.controller,
|
|
216
|
+
action: route.action,
|
|
217
|
+
params: extracted_params,
|
|
218
|
+
namespace: route.namespace
|
|
219
|
+
}
|
|
192
220
|
end
|
|
193
221
|
|
|
194
222
|
# Si llegamos aquí, es un 404 seguro.
|
|
@@ -220,10 +248,11 @@ module BugBunny
|
|
|
220
248
|
end
|
|
221
249
|
|
|
222
250
|
if final_to.nil?
|
|
223
|
-
raise ArgumentError,
|
|
251
|
+
raise ArgumentError,
|
|
252
|
+
"Falta el destino 'to:' para la ruta #{method} '#{final_path}'. Usa la sintaxis 'controlador#accion'"
|
|
224
253
|
end
|
|
225
254
|
|
|
226
|
-
@routes << Route.new(method, final_path, to: final_to)
|
|
255
|
+
@routes << Route.new(method, final_path, to: final_to, namespace: current_namespace)
|
|
227
256
|
end
|
|
228
257
|
|
|
229
258
|
# --- LÓGICA DE SCOPES INTERNOS ---
|
|
@@ -263,6 +292,15 @@ module BugBunny
|
|
|
263
292
|
end
|
|
264
293
|
nil
|
|
265
294
|
end
|
|
295
|
+
|
|
296
|
+
# Calcula el namespace acumulado recorriendo la pila de scopes.
|
|
297
|
+
#
|
|
298
|
+
# @return [String, nil] El namespace (ej: "Api::V1") o nil si no hay.
|
|
299
|
+
# @api private
|
|
300
|
+
def current_namespace
|
|
301
|
+
parts = @scopes.select { |s| s[:type] == :namespace }.map { |s| s[:name] }
|
|
302
|
+
parts.empty? ? nil : parts.join('::')
|
|
303
|
+
end
|
|
266
304
|
end
|
|
267
305
|
end
|
|
268
306
|
end
|
data/lib/bug_bunny/session.rb
CHANGED
|
@@ -9,6 +9,7 @@ module BugBunny
|
|
|
9
9
|
# @api private
|
|
10
10
|
class Session
|
|
11
11
|
include BugBunny::Observability
|
|
12
|
+
|
|
12
13
|
# @!group Opciones por Defecto (Nivel 1: Gema)
|
|
13
14
|
|
|
14
15
|
# Opciones predeterminadas de la gema para Exchanges.
|
|
@@ -32,6 +33,7 @@ module BugBunny
|
|
|
32
33
|
@connection = connection
|
|
33
34
|
@publisher_confirms = publisher_confirms
|
|
34
35
|
@channel = nil
|
|
36
|
+
@channel_mutex = Mutex.new
|
|
35
37
|
@logger = BugBunny.configuration.logger
|
|
36
38
|
end
|
|
37
39
|
|
|
@@ -43,12 +45,17 @@ module BugBunny
|
|
|
43
45
|
# @return [Bunny::Channel] Un canal abierto y configurado.
|
|
44
46
|
# @raise [BugBunny::CommunicationError] Si no se puede restablecer la conexión.
|
|
45
47
|
def channel
|
|
46
|
-
#
|
|
48
|
+
# Fast path: canal abierto, sin adquirir el mutex.
|
|
47
49
|
return @channel if @channel&.open?
|
|
48
50
|
|
|
49
|
-
#
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
# Slow path: adquirimos el mutex y verificamos de nuevo (double-checked locking).
|
|
52
|
+
# Evita que múltiples threads creen canales simultáneamente cuando el canal cae.
|
|
53
|
+
@channel_mutex.synchronize do
|
|
54
|
+
return @channel if @channel&.open?
|
|
55
|
+
|
|
56
|
+
ensure_connection!
|
|
57
|
+
create_channel!
|
|
58
|
+
end
|
|
52
59
|
|
|
53
60
|
@channel
|
|
54
61
|
end
|
|
@@ -98,8 +105,10 @@ module BugBunny
|
|
|
98
105
|
# Cierra el canal asociado a esta sesión de forma segura.
|
|
99
106
|
# @return [void]
|
|
100
107
|
def close
|
|
101
|
-
@
|
|
102
|
-
|
|
108
|
+
@channel_mutex.synchronize do
|
|
109
|
+
@channel&.close if @channel&.open?
|
|
110
|
+
@channel = nil
|
|
111
|
+
end
|
|
103
112
|
end
|
|
104
113
|
|
|
105
114
|
private
|
|
@@ -113,9 +122,7 @@ module BugBunny
|
|
|
113
122
|
|
|
114
123
|
@channel.confirm_select if @publisher_confirms
|
|
115
124
|
|
|
116
|
-
if BugBunny.configuration.channel_prefetch
|
|
117
|
-
@channel.prefetch(BugBunny.configuration.channel_prefetch)
|
|
118
|
-
end
|
|
125
|
+
@channel.prefetch(BugBunny.configuration.channel_prefetch) if BugBunny.configuration.channel_prefetch
|
|
119
126
|
rescue StandardError => e
|
|
120
127
|
raise BugBunny::CommunicationError, "Failed to create channel: #{e.message}"
|
|
121
128
|
end
|
|
@@ -127,10 +134,10 @@ module BugBunny
|
|
|
127
134
|
def ensure_connection!
|
|
128
135
|
return if @connection.open?
|
|
129
136
|
|
|
130
|
-
safe_log(:warn,
|
|
137
|
+
safe_log(:warn, 'session.reconnect_attempt')
|
|
131
138
|
@connection.start
|
|
132
139
|
rescue StandardError => e
|
|
133
|
-
safe_log(:error,
|
|
140
|
+
safe_log(:error, 'session.reconnect_failed', **exception_metadata(e))
|
|
134
141
|
raise BugBunny::CommunicationError, "Could not reconnect to RabbitMQ: #{e.message}"
|
|
135
142
|
end
|
|
136
143
|
end
|
data/lib/bug_bunny/version.rb
CHANGED
data/lib/bug_bunny.rb
CHANGED
|
@@ -25,6 +25,7 @@ require_relative 'bug_bunny/railtie' if defined?(Rails)
|
|
|
25
25
|
# Actúa como espacio de nombres y punto de configuración global.
|
|
26
26
|
module BugBunny
|
|
27
27
|
extend BugBunny::Observability
|
|
28
|
+
|
|
28
29
|
private_class_method :safe_log, :exception_metadata, :observability_name
|
|
29
30
|
|
|
30
31
|
class << self
|
|
@@ -56,6 +57,7 @@ module BugBunny
|
|
|
56
57
|
def self.configure
|
|
57
58
|
self.configuration ||= Configuration.new
|
|
58
59
|
yield(configuration)
|
|
60
|
+
configuration.validate!
|
|
59
61
|
end
|
|
60
62
|
|
|
61
63
|
# Crea e inicia una nueva conexión a RabbitMQ utilizando la gema Bunny.
|
|
@@ -94,9 +96,9 @@ module BugBunny
|
|
|
94
96
|
|
|
95
97
|
@global_connection.close if @global_connection.open?
|
|
96
98
|
@global_connection = nil
|
|
97
|
-
|
|
99
|
+
|
|
98
100
|
@logger = configuration.logger
|
|
99
|
-
safe_log(:info,
|
|
101
|
+
safe_log(:info, 'bug_bunny.disconnect')
|
|
100
102
|
end
|
|
101
103
|
|
|
102
104
|
# @api private
|
|
@@ -20,7 +20,7 @@ module BugBunny
|
|
|
20
20
|
# @api private
|
|
21
21
|
source_root File.expand_path('templates', __dir__)
|
|
22
22
|
|
|
23
|
-
desc
|
|
23
|
+
desc 'Instala la configuración inicial de BugBunny y crea la estructura de directorios.'
|
|
24
24
|
|
|
25
25
|
# Genera el archivo de configuración inicial.
|
|
26
26
|
# Copia la plantilla `initializer.rb` a `config/initializers/bug_bunny.rb` en la app destino.
|
|
@@ -33,15 +33,55 @@ module BugBunny
|
|
|
33
33
|
# Crea la estructura de carpetas necesaria para el patrón MVC de BugBunny.
|
|
34
34
|
#
|
|
35
35
|
# Genera:
|
|
36
|
-
# * `app/
|
|
36
|
+
# * `app/bug_bunny/controllers/`: Directorio donde vivirán los controladores de consumidores.
|
|
37
37
|
# * `.keep`: Archivo marcador para asegurar que Git rastree la carpeta aunque esté vacía.
|
|
38
38
|
#
|
|
39
39
|
# @return [void]
|
|
40
40
|
def create_directories
|
|
41
|
-
empty_directory
|
|
42
|
-
create_file
|
|
41
|
+
empty_directory 'app/bug_bunny/controllers'
|
|
42
|
+
create_file 'app/bug_bunny/controllers/.keep', ''
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Escribe el bloque inicial de BugBunny en CLAUDE.md del proyecto consumidor.
|
|
46
|
+
#
|
|
47
|
+
# Si CLAUDE.md no existe, crea uno mínimo con la sección correspondiente.
|
|
48
|
+
# Si ya existe, agrega la sección `## Gemas internas` con el bloque de bug_bunny.
|
|
49
|
+
# En ambos casos, el rake task `bug_bunny:sync` se encarga de las actualizaciones futuras.
|
|
50
|
+
#
|
|
51
|
+
# @return [void]
|
|
52
|
+
def update_claude_md
|
|
53
|
+
spec = Gem::Specification.find_by_name('bug_bunny')
|
|
54
|
+
version = spec.version.to_s
|
|
55
|
+
docs_path = File.join(spec.gem_dir, 'docs', 'ai')
|
|
56
|
+
|
|
57
|
+
block = <<~BLOCK
|
|
58
|
+
## Gemas internas
|
|
59
|
+
|
|
60
|
+
### bug_bunny
|
|
61
|
+
- **Version:** #{version}
|
|
62
|
+
- **Docs:** #{docs_path}
|
|
63
|
+
- **Updated:** #{Time.now.strftime('%Y-%m-%d')}
|
|
64
|
+
BLOCK
|
|
65
|
+
|
|
66
|
+
claude_md = File.join(destination_root, 'CLAUDE.md')
|
|
67
|
+
|
|
68
|
+
if File.exist?(claude_md)
|
|
69
|
+
content = File.read(claude_md)
|
|
70
|
+
if content.include?('### bug_bunny')
|
|
71
|
+
say_status :skip, 'CLAUDE.md already has a bug_bunny block — run `rake bug_bunny:sync` to update'
|
|
72
|
+
elsif content.include?('## Gemas internas')
|
|
73
|
+
inject_into_file 'CLAUDE.md', "\n#{block.lines.drop(2).join}", after: "## Gemas internas\n"
|
|
74
|
+
say_status :update, 'Added bug_bunny block to existing ## Gemas internas section in CLAUDE.md'
|
|
75
|
+
else
|
|
76
|
+
append_to_file 'CLAUDE.md', "\n#{block}"
|
|
77
|
+
say_status :update, 'Added ## Gemas internas section to CLAUDE.md'
|
|
78
|
+
end
|
|
79
|
+
else
|
|
80
|
+
create_file 'CLAUDE.md', "# #{Rails.application.class.module_parent_name}\n\n#{block}"
|
|
81
|
+
say_status :create, 'CLAUDE.md created with bug_bunny block'
|
|
82
|
+
end
|
|
43
83
|
|
|
44
|
-
|
|
84
|
+
say ' Add `bundle exec rake bug_bunny:sync` to bin/setup to keep this block up to date.'
|
|
45
85
|
end
|
|
46
86
|
end
|
|
47
87
|
end
|