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/producer.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'concurrent'
|
|
2
4
|
require 'json'
|
|
3
5
|
require 'securerandom'
|
|
@@ -27,39 +29,30 @@ module BugBunny
|
|
|
27
29
|
|
|
28
30
|
# Envía un mensaje de forma asíncrona (Fire-and-Forget).
|
|
29
31
|
#
|
|
30
|
-
# Serializa el cuerpo del request, resuelve el exchange
|
|
31
|
-
#
|
|
32
|
+
# Serializa el cuerpo del request, resuelve el exchange aplicando la cascada de
|
|
33
|
+
# configuración y publica el mensaje sin esperar respuesta.
|
|
32
34
|
#
|
|
33
|
-
# @param request [BugBunny::Request] Objeto con la configuración del envío (body,
|
|
35
|
+
# @param request [BugBunny::Request] Objeto con la configuración del envío (body, exchange_options, etc).
|
|
34
36
|
# @return [void]
|
|
35
37
|
def fire(request)
|
|
36
|
-
|
|
38
|
+
# Obtenemos el exchange pasando las opciones específicas del request para la fusión en cascada
|
|
39
|
+
x = @session.exchange(
|
|
40
|
+
name: request.exchange,
|
|
41
|
+
type: request.exchange_type,
|
|
42
|
+
opts: request.exchange_options
|
|
43
|
+
)
|
|
44
|
+
|
|
37
45
|
payload = serialize_message(request.body)
|
|
38
46
|
opts = request.amqp_options
|
|
39
47
|
|
|
40
|
-
|
|
41
|
-
# Muestra claramente: Verbo, Recurso, Exchange (y su tipo) y la Routing Key usada.
|
|
42
|
-
verb = request.method.to_s.upcase
|
|
43
|
-
target = request.path
|
|
44
|
-
ex_info = "'#{request.exchange}' (Type: #{request.exchange_type})"
|
|
45
|
-
rk = request.final_routing_key
|
|
46
|
-
|
|
47
|
-
BugBunny.configuration.logger.info("[BugBunny] [#{verb}] '/#{target}' | Exchange: #{ex_info} | Routing Key: '#{rk}'")
|
|
48
|
+
log_request(request, payload)
|
|
48
49
|
|
|
49
50
|
x.publish(payload, opts.merge(routing_key: request.final_routing_key))
|
|
50
51
|
end
|
|
51
52
|
|
|
52
53
|
# Envía un mensaje y bloquea el hilo actual esperando una respuesta (RPC).
|
|
53
54
|
#
|
|
54
|
-
# Implementa el mecanismo "Direct Reply-to" de RabbitMQ (`amq.rabbitmq.reply-to`)
|
|
55
|
-
# para recibir la respuesta directamente sin necesidad de crear colas temporales
|
|
56
|
-
# por cada petición, lo cual mejora significativamente el rendimiento.
|
|
57
|
-
#
|
|
58
|
-
# El flujo es:
|
|
59
|
-
# 1. Asegura que hay un consumidor escuchando en `amq.rabbitmq.reply-to`.
|
|
60
|
-
# 2. Genera un `correlation_id` único.
|
|
61
|
-
# 3. Crea una promesa (`Concurrent::IVar`) y la registra.
|
|
62
|
-
# 4. Publica el mensaje y bloquea esperando que la promesa se resuelva.
|
|
55
|
+
# Implementa el mecanismo "Direct Reply-to" de RabbitMQ (`amq.rabbitmq.reply-to`).
|
|
63
56
|
#
|
|
64
57
|
# @param request [BugBunny::Request] Objeto request configurado.
|
|
65
58
|
# @return [Hash] El cuerpo de la respuesta parseado desde JSON.
|
|
@@ -80,11 +73,12 @@ module BugBunny
|
|
|
80
73
|
begin
|
|
81
74
|
fire(request)
|
|
82
75
|
|
|
76
|
+
BugBunny.configuration.logger.debug("[BugBunny::Producer] ⏳ Waiting for RPC response | ID: #{cid} | Timeout: #{wait_timeout}s")
|
|
77
|
+
|
|
83
78
|
# Bloqueamos el hilo aquí hasta que llegue la respuesta o expire el timeout
|
|
84
79
|
response_payload = future.value(wait_timeout)
|
|
85
80
|
|
|
86
81
|
if response_payload.nil?
|
|
87
|
-
# CORRECCIÓN: Usamos request.path y request.method en lugar de request.action
|
|
88
82
|
raise BugBunny::RequestTimeout, "Timeout waiting for RPC: #{request.path} [#{request.method}]"
|
|
89
83
|
end
|
|
90
84
|
|
|
@@ -97,7 +91,31 @@ module BugBunny
|
|
|
97
91
|
|
|
98
92
|
private
|
|
99
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.
|
|
98
|
+
def log_request(request, payload)
|
|
99
|
+
verb = request.method.to_s.upcase
|
|
100
|
+
target = request.path
|
|
101
|
+
rk = request.final_routing_key
|
|
102
|
+
id = request.correlation_id
|
|
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
|
+
|
|
109
|
+
# INFO: Resumen de una línea (Traffic)
|
|
110
|
+
BugBunny.configuration.logger.info("[BugBunny::Producer] 📤 #{verb} /#{target} | RK: '#{rk}' | ID: #{id}")
|
|
111
|
+
|
|
112
|
+
# DEBUG: Detalle completo de Infraestructura y Payload
|
|
113
|
+
BugBunny.configuration.logger.debug("[BugBunny::Producer] ⚙️ Exchange Opts: #{final_x_opts}")
|
|
114
|
+
BugBunny.configuration.logger.debug("[BugBunny::Producer] 📦 Payload: #{payload.truncate(300)}") if payload.is_a?(String)
|
|
115
|
+
end
|
|
116
|
+
|
|
100
117
|
# Serializa el mensaje para su transporte.
|
|
118
|
+
#
|
|
101
119
|
# @param msg [Hash, String, Object] El mensaje a serializar.
|
|
102
120
|
# @return [String] Cadena JSON o string crudo.
|
|
103
121
|
def serialize_message(msg)
|
|
@@ -105,6 +123,9 @@ module BugBunny
|
|
|
105
123
|
end
|
|
106
124
|
|
|
107
125
|
# Intenta parsear la respuesta recibida.
|
|
126
|
+
#
|
|
127
|
+
# @param payload [String] El cuerpo de la respuesta recibida.
|
|
128
|
+
# @return [Hash] El JSON parseado.
|
|
108
129
|
# @raise [BugBunny::InternalServerError] Si el payload no es JSON válido.
|
|
109
130
|
def parse_response(payload)
|
|
110
131
|
JSON.parse(payload)
|
|
@@ -115,27 +136,25 @@ module BugBunny
|
|
|
115
136
|
# Inicia el consumidor de respuestas RPC de forma perezosa (Lazy Initialization).
|
|
116
137
|
#
|
|
117
138
|
# Utiliza un patrón de "Double-Checked Locking" con Mutex para asegurar que
|
|
118
|
-
# solo se crea un listener por instancia de Producer
|
|
139
|
+
# solo se crea un listener por instancia de Producer.
|
|
119
140
|
#
|
|
120
|
-
#
|
|
121
|
-
# busca el `correlation_id` en el mapa de pendientes y completa el futuro (`IVar`),
|
|
122
|
-
# desbloqueando así al hilo que llamó a {#rpc}.
|
|
141
|
+
# @return [void]
|
|
123
142
|
def ensure_reply_listener!
|
|
124
143
|
return if @reply_listener_started
|
|
125
144
|
|
|
126
145
|
@reply_listener_mutex.synchronize do
|
|
127
146
|
return if @reply_listener_started
|
|
128
147
|
|
|
129
|
-
BugBunny.configuration.logger.debug("[Producer] 👂
|
|
148
|
+
BugBunny.configuration.logger.debug("[BugBunny::Producer] 👂 Starting Reply Listener on 'amq.rabbitmq.reply-to'")
|
|
130
149
|
|
|
131
150
|
# Consumimos sin ack (auto-ack) porque reply-to no soporta acks manuales de forma estándar
|
|
132
151
|
@session.channel.basic_consume('amq.rabbitmq.reply-to', '', true, false, nil) do |_, props, body|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (future = @pending_requests[
|
|
152
|
+
cid = props.correlation_id.to_s
|
|
153
|
+
BugBunny.configuration.logger.debug("[BugBunny::Producer] 📥 RPC Response matched | ID: #{cid}")
|
|
154
|
+
if (future = @pending_requests[cid])
|
|
136
155
|
future.set(body)
|
|
137
|
-
|
|
138
|
-
|
|
156
|
+
else
|
|
157
|
+
BugBunny.configuration.logger.warn("[BugBunny::Producer] ⚠️ Orphaned RPC Response received | ID: #{cid}")
|
|
139
158
|
end
|
|
140
159
|
end
|
|
141
160
|
@reply_listener_started = true
|
data/lib/bug_bunny/request.rb
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
#
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module BugBunny
|
|
4
4
|
# Encapsula toda la información necesaria para realizar una petición o publicación.
|
|
5
5
|
#
|
|
6
6
|
# Actúa como el objeto "Environment" en la arquitectura de middlewares.
|
|
7
|
-
# Contiene el cuerpo del mensaje, la configuración de enrutamiento
|
|
7
|
+
# Contiene el cuerpo del mensaje, la configuración de enrutamiento, el **Verbo HTTP**
|
|
8
|
+
# y las opciones de infraestructura específicas para la petición.
|
|
8
9
|
#
|
|
9
10
|
# @attr body [Object] El cuerpo del mensaje (Hash, Array o String).
|
|
10
11
|
# @attr headers [Hash] Cabeceras personalizadas (Headers AMQP).
|
|
@@ -14,6 +15,9 @@ module BugBunny
|
|
|
14
15
|
# @attr exchange_type [String] El tipo de exchange ('direct', 'topic', 'fanout').
|
|
15
16
|
# @attr routing_key [String] La routing key específica. Si es nil, se usará {#path}.
|
|
16
17
|
# @attr timeout [Integer] Tiempo máximo en segundos para timeout RPC.
|
|
18
|
+
#
|
|
19
|
+
# @attr exchange_options [Hash] Opciones específicas para la declaración del Exchange en esta petición.
|
|
20
|
+
# @attr queue_options [Hash] Opciones específicas para la declaración de la Cola en esta petición.
|
|
17
21
|
class Request
|
|
18
22
|
attr_accessor :body
|
|
19
23
|
attr_accessor :headers
|
|
@@ -24,6 +28,10 @@ module BugBunny
|
|
|
24
28
|
attr_accessor :routing_key
|
|
25
29
|
attr_accessor :timeout
|
|
26
30
|
|
|
31
|
+
# Configuración de Infraestructura Específica
|
|
32
|
+
attr_accessor :exchange_options
|
|
33
|
+
attr_accessor :queue_options
|
|
34
|
+
|
|
27
35
|
# Metadatos AMQP Estándar
|
|
28
36
|
attr_accessor :app_id, :content_type, :content_encoding, :priority,
|
|
29
37
|
:timestamp, :expiration, :persistent, :reply_to,
|
|
@@ -40,6 +48,10 @@ module BugBunny
|
|
|
40
48
|
@timestamp = Time.now.to_i
|
|
41
49
|
@persistent = false
|
|
42
50
|
@exchange_type = 'direct'
|
|
51
|
+
|
|
52
|
+
# Inicialización de opciones de infraestructura para evitar errores de nil durante el merge.
|
|
53
|
+
@exchange_options = {}
|
|
54
|
+
@queue_options = {}
|
|
43
55
|
end
|
|
44
56
|
|
|
45
57
|
# Calcula la Routing Key final que se usará en RabbitMQ.
|
data/lib/bug_bunny/resource.rb
CHANGED
|
@@ -9,15 +9,13 @@ require 'rack/utils'
|
|
|
9
9
|
module BugBunny
|
|
10
10
|
# Clase base para modelos remotos que implementan **Active Record over AMQP (RESTful)**.
|
|
11
11
|
#
|
|
12
|
-
# Soporta un esquema híbrido de datos:
|
|
13
|
-
# 1. **
|
|
14
|
-
# 2. **
|
|
15
|
-
#
|
|
16
|
-
# Implementa un sistema de "Dirty Tracking" híbrido para detectar cambios
|
|
17
|
-
# tanto en atributos tipados (via ActiveModel) como dinámicos (via Set manual).
|
|
12
|
+
# Soporta un esquema híbrido de datos y configuración de infraestructura en cascada:
|
|
13
|
+
# 1. **Defaults:** Definidos en la sesión.
|
|
14
|
+
# 2. **Global:** Definidos en BugBunny.configuration.
|
|
15
|
+
# 3. **Específico:** Definidos en la clase del recurso o vía `with`.
|
|
18
16
|
#
|
|
19
17
|
# @author Gabriel
|
|
20
|
-
# @since 3.0
|
|
18
|
+
# @since 3.1.0
|
|
21
19
|
class Resource
|
|
22
20
|
include ActiveModel::API
|
|
23
21
|
include ActiveModel::Attributes
|
|
@@ -31,11 +29,22 @@ module BugBunny
|
|
|
31
29
|
attr_accessor :persisted
|
|
32
30
|
attr_accessor :routing_key, :exchange, :exchange_type
|
|
33
31
|
|
|
32
|
+
# @return [Hash] Opciones específicas de instancia para exchange y queue.
|
|
33
|
+
attr_accessor :exchange_options, :queue_options
|
|
34
|
+
|
|
34
35
|
class << self
|
|
35
36
|
attr_writer :connection_pool, :exchange, :exchange_type, :resource_name, :routing_key, :param_key
|
|
36
37
|
|
|
38
|
+
# @!group Configuración de Infraestructura Específica
|
|
39
|
+
attr_writer :exchange_options, :queue_options
|
|
40
|
+
|
|
41
|
+
# @api private
|
|
37
42
|
def thread_config(key); Thread.current["bb_#{object_id}_#{key}"]; end
|
|
38
43
|
|
|
44
|
+
# Resuelve la configuración buscando en el hilo, luego en la jerarquía de clases.
|
|
45
|
+
# @param key [Symbol] Clave en el Thread.current.
|
|
46
|
+
# @param instance_var [Symbol] Nombre de la variable de instancia en la clase.
|
|
47
|
+
# @return [Object, nil]
|
|
39
48
|
def resolve_config(key, instance_var)
|
|
40
49
|
val = thread_config(key)
|
|
41
50
|
return val if val
|
|
@@ -48,23 +57,38 @@ module BugBunny
|
|
|
48
57
|
nil
|
|
49
58
|
end
|
|
50
59
|
|
|
60
|
+
# @return [ConnectionPool, nil]
|
|
51
61
|
def connection_pool; resolve_config(:pool, :@connection_pool); end
|
|
52
|
-
|
|
62
|
+
|
|
63
|
+
# @return [String] Nombre del exchange actual.
|
|
64
|
+
def current_exchange; resolve_config(:exchange, :@exchange) || raise(ArgumentError, "Exchange not defined for #{name}"); end
|
|
65
|
+
|
|
66
|
+
# @return [String] Tipo de exchange ('direct', 'topic', 'fanout').
|
|
53
67
|
def current_exchange_type; resolve_config(:exchange_type, :@exchange_type) || 'direct'; end
|
|
54
68
|
|
|
69
|
+
# @return [Hash] Opciones de exchange específicas (Nivel 3 de la cascada).
|
|
70
|
+
def current_exchange_options; resolve_config(:exchange_options, :@exchange_options) || {}; end
|
|
71
|
+
|
|
72
|
+
# @return [Hash] Opciones de cola específicas.
|
|
73
|
+
def current_queue_options; resolve_config(:queue_options, :@queue_options) || {}; end
|
|
74
|
+
|
|
75
|
+
# @return [String] Nombre del recurso para la construcción de rutas.
|
|
55
76
|
def resource_name
|
|
56
77
|
resolve_config(:resource_name, :@resource_name) || name.demodulize.underscore.pluralize
|
|
57
78
|
end
|
|
58
79
|
|
|
80
|
+
# @return [String] Clave raíz para envolver el payload en las peticiones.
|
|
59
81
|
def param_key
|
|
60
82
|
resolve_config(:param_key, :@param_key) || model_name.element
|
|
61
83
|
end
|
|
62
84
|
|
|
85
|
+
# @api private
|
|
63
86
|
def client_middleware(&block)
|
|
64
87
|
@client_middleware_stack ||= []
|
|
65
88
|
@client_middleware_stack << block
|
|
66
89
|
end
|
|
67
90
|
|
|
91
|
+
# @api private
|
|
68
92
|
def resolve_middleware_stack
|
|
69
93
|
stack = []
|
|
70
94
|
target = self
|
|
@@ -76,6 +100,8 @@ module BugBunny
|
|
|
76
100
|
stack
|
|
77
101
|
end
|
|
78
102
|
|
|
103
|
+
# Instancia el cliente inyectando los middlewares configurados.
|
|
104
|
+
# @return [BugBunny::Client]
|
|
79
105
|
def bug_bunny_client
|
|
80
106
|
pool = connection_pool
|
|
81
107
|
raise BugBunny::Error, "Connection pool missing for #{name}" unless pool
|
|
@@ -84,8 +110,23 @@ module BugBunny
|
|
|
84
110
|
end
|
|
85
111
|
end
|
|
86
112
|
|
|
87
|
-
|
|
88
|
-
|
|
113
|
+
# Permite configurar dinámicamente el contexto AMQP para una operación.
|
|
114
|
+
#
|
|
115
|
+
# @param exchange [String] Nombre del exchange.
|
|
116
|
+
# @param routing_key [String] Routing key manual.
|
|
117
|
+
# @param exchange_type [String] Tipo de exchange.
|
|
118
|
+
# @param pool [ConnectionPool] Pool de conexiones.
|
|
119
|
+
# @param exchange_options [Hash] Opciones de infraestructura.
|
|
120
|
+
# @param queue_options [Hash] Opciones de cola.
|
|
121
|
+
def with(exchange: nil, routing_key: nil, exchange_type: nil, pool: nil, exchange_options: nil, queue_options: nil)
|
|
122
|
+
keys = {
|
|
123
|
+
exchange: "bb_#{object_id}_exchange",
|
|
124
|
+
exchange_type: "bb_#{object_id}_exchange_type",
|
|
125
|
+
pool: "bb_#{object_id}_pool",
|
|
126
|
+
routing_key: "bb_#{object_id}_routing_key",
|
|
127
|
+
exchange_options: "bb_#{object_id}_exchange_options",
|
|
128
|
+
queue_options: "bb_#{object_id}_queue_options"
|
|
129
|
+
}
|
|
89
130
|
old_values = {}
|
|
90
131
|
keys.each { |k, v| old_values[k] = Thread.current[v] }
|
|
91
132
|
|
|
@@ -93,6 +134,8 @@ module BugBunny
|
|
|
93
134
|
Thread.current[keys[:exchange_type]] = exchange_type if exchange_type
|
|
94
135
|
Thread.current[keys[:pool]] = pool if pool
|
|
95
136
|
Thread.current[keys[:routing_key]] = routing_key if routing_key
|
|
137
|
+
Thread.current[keys[:exchange_options]] = exchange_options if exchange_options
|
|
138
|
+
Thread.current[keys[:queue_options]] = queue_options if queue_options
|
|
96
139
|
|
|
97
140
|
if block_given?
|
|
98
141
|
begin; yield; ensure; keys.each { |k, v| Thread.current[v] = old_values[k] }; end
|
|
@@ -101,11 +144,15 @@ module BugBunny
|
|
|
101
144
|
end
|
|
102
145
|
end
|
|
103
146
|
|
|
147
|
+
# Proxy para el encadenamiento del método `.with`.
|
|
104
148
|
class ScopeProxy < BasicObject
|
|
105
149
|
def initialize(target, keys, old_values); @target = target; @keys = keys; @old_values = old_values; end
|
|
106
150
|
def method_missing(method, *args, &block); @target.public_send(method, *args, &block); ensure; @keys.each { |k, v| ::Thread.current[v] = @old_values[k] }; end
|
|
107
151
|
end
|
|
108
152
|
|
|
153
|
+
# Calcula la routing key final.
|
|
154
|
+
# @param id [String, nil] ID del recurso.
|
|
155
|
+
# @return [String]
|
|
109
156
|
def calculate_routing_key(id = nil)
|
|
110
157
|
manual_rk = thread_config(:routing_key)
|
|
111
158
|
return manual_rk if manual_rk
|
|
@@ -115,11 +162,25 @@ module BugBunny
|
|
|
115
162
|
end
|
|
116
163
|
|
|
117
164
|
# @!group Acciones CRUD RESTful
|
|
165
|
+
|
|
166
|
+
# Realiza una búsqueda filtrada (GET).
|
|
167
|
+
# @param filters [Hash]
|
|
168
|
+
# @return [Array<BugBunny::Resource>]
|
|
118
169
|
def where(filters = {})
|
|
119
170
|
rk = calculate_routing_key
|
|
120
171
|
path = resource_name
|
|
121
172
|
path += "?#{Rack::Utils.build_nested_query(filters)}" if filters.present?
|
|
122
|
-
|
|
173
|
+
|
|
174
|
+
response = bug_bunny_client.request(
|
|
175
|
+
path,
|
|
176
|
+
method: :get,
|
|
177
|
+
exchange: current_exchange,
|
|
178
|
+
exchange_type: current_exchange_type,
|
|
179
|
+
routing_key: rk,
|
|
180
|
+
exchange_options: current_exchange_options,
|
|
181
|
+
queue_options: current_queue_options
|
|
182
|
+
)
|
|
183
|
+
|
|
123
184
|
return [] unless response['body'].is_a?(Array)
|
|
124
185
|
response['body'].map do |attrs|
|
|
125
186
|
inst = new(attrs)
|
|
@@ -129,12 +190,27 @@ module BugBunny
|
|
|
129
190
|
end
|
|
130
191
|
end
|
|
131
192
|
|
|
193
|
+
# Devuelve todos los registros.
|
|
194
|
+
# @return [Array<BugBunny::Resource>]
|
|
132
195
|
def all; where({}); end
|
|
133
196
|
|
|
197
|
+
# Busca un registro por ID (GET).
|
|
198
|
+
# @param id [String, Integer]
|
|
199
|
+
# @return [BugBunny::Resource, nil]
|
|
134
200
|
def find(id)
|
|
135
201
|
rk = calculate_routing_key(id)
|
|
136
202
|
path = "#{resource_name}/#{id}"
|
|
137
|
-
|
|
203
|
+
|
|
204
|
+
response = bug_bunny_client.request(
|
|
205
|
+
path,
|
|
206
|
+
method: :get,
|
|
207
|
+
exchange: current_exchange,
|
|
208
|
+
exchange_type: current_exchange_type,
|
|
209
|
+
routing_key: rk,
|
|
210
|
+
exchange_options: current_exchange_options,
|
|
211
|
+
queue_options: current_queue_options
|
|
212
|
+
)
|
|
213
|
+
|
|
138
214
|
return nil if response.nil? || response['status'] == 404
|
|
139
215
|
return nil unless response['body'].is_a?(Hash)
|
|
140
216
|
instance = new(response['body'])
|
|
@@ -143,6 +219,9 @@ module BugBunny
|
|
|
143
219
|
instance
|
|
144
220
|
end
|
|
145
221
|
|
|
222
|
+
# Crea una nueva instancia y la persiste.
|
|
223
|
+
# @param payload [Hash]
|
|
224
|
+
# @return [BugBunny::Resource]
|
|
146
225
|
def create(payload)
|
|
147
226
|
instance = new(payload)
|
|
148
227
|
instance.save
|
|
@@ -152,13 +231,19 @@ module BugBunny
|
|
|
152
231
|
|
|
153
232
|
# @!group Instancia
|
|
154
233
|
|
|
234
|
+
# Inicializa el recurso.
|
|
235
|
+
# @param attributes [Hash]
|
|
155
236
|
def initialize(attributes = {})
|
|
156
237
|
@remote_attributes = {}.with_indifferent_access
|
|
157
238
|
@dynamic_changes = Set.new # Rastreo manual para atributos dinámicos
|
|
158
239
|
@persisted = false
|
|
240
|
+
|
|
241
|
+
# Contexto de infraestructura
|
|
159
242
|
@routing_key = self.class.thread_config(:routing_key)
|
|
160
243
|
@exchange = self.class.thread_config(:exchange)
|
|
161
244
|
@exchange_type = self.class.thread_config(:exchange_type)
|
|
245
|
+
@exchange_options = self.class.thread_config(:exchange_options) || self.class.current_exchange_options
|
|
246
|
+
@queue_options = self.class.thread_config(:queue_options) || self.class.current_queue_options
|
|
162
247
|
|
|
163
248
|
super()
|
|
164
249
|
assign_attributes(attributes)
|
|
@@ -170,30 +255,46 @@ module BugBunny
|
|
|
170
255
|
@dynamic_changes.clear
|
|
171
256
|
end
|
|
172
257
|
|
|
258
|
+
# Serialización combinada.
|
|
259
|
+
# @return [Hash]
|
|
173
260
|
def attributes_for_serialization
|
|
174
261
|
@remote_attributes.merge(attributes)
|
|
175
262
|
end
|
|
176
263
|
|
|
264
|
+
# @return [String]
|
|
177
265
|
def calculate_routing_key(id=nil); @routing_key || self.class.calculate_routing_key(id); end
|
|
266
|
+
|
|
267
|
+
# @return [String]
|
|
178
268
|
def current_exchange; @exchange || self.class.current_exchange; end
|
|
269
|
+
|
|
270
|
+
# @return [String]
|
|
179
271
|
def current_exchange_type; @exchange_type || self.class.current_exchange_type; end
|
|
272
|
+
|
|
273
|
+
# @return [BugBunny::Client]
|
|
180
274
|
def bug_bunny_client; self.class.bug_bunny_client; end
|
|
275
|
+
|
|
276
|
+
# @return [Boolean]
|
|
181
277
|
def persisted?; !!@persisted; end
|
|
182
278
|
|
|
279
|
+
# Asignación masiva de atributos.
|
|
280
|
+
# @param new_attributes [Hash]
|
|
183
281
|
def assign_attributes(new_attributes)
|
|
184
282
|
return if new_attributes.nil?
|
|
185
283
|
new_attributes.each { |k, v| public_send("#{k}=", v) }
|
|
186
284
|
end
|
|
187
285
|
|
|
286
|
+
# Actualiza y guarda.
|
|
287
|
+
# @param attributes [Hash]
|
|
288
|
+
# @return [Boolean]
|
|
188
289
|
def update(attributes)
|
|
189
290
|
assign_attributes(attributes)
|
|
190
291
|
save
|
|
191
292
|
end
|
|
192
293
|
|
|
193
294
|
# Retorna el hash combinado de cambios (Tipados + Dinámicos).
|
|
295
|
+
# @return [Hash]
|
|
194
296
|
def changes_to_send
|
|
195
297
|
# 1. Cambios de ActiveModel (Tipados)
|
|
196
|
-
# changes returns { 'attr' => [old, new] } -> nos quedamos con new
|
|
197
298
|
payload = changes.transform_values(&:last)
|
|
198
299
|
|
|
199
300
|
# 2. Cambios Dinámicos (Manuales)
|
|
@@ -214,8 +315,6 @@ module BugBunny
|
|
|
214
315
|
key = attribute_name.chop
|
|
215
316
|
val = args.first
|
|
216
317
|
|
|
217
|
-
# Dirty Tracking Manual
|
|
218
|
-
# Si el valor cambia, lo marcamos en nuestro Set
|
|
219
318
|
if @remote_attributes[key] != val
|
|
220
319
|
@dynamic_changes << key
|
|
221
320
|
end
|
|
@@ -230,6 +329,7 @@ module BugBunny
|
|
|
230
329
|
@remote_attributes.key?(method_name.to_s.sub(/=$/, '')) || super
|
|
231
330
|
end
|
|
232
331
|
|
|
332
|
+
# @return [Object] Valor del ID buscando en múltiples nomenclaturas.
|
|
233
333
|
def id
|
|
234
334
|
attributes['id'] || @remote_attributes['id'] || @remote_attributes['ID'] || @remote_attributes['Id'] || @remote_attributes['_id']
|
|
235
335
|
end
|
|
@@ -249,6 +349,8 @@ module BugBunny
|
|
|
249
349
|
|
|
250
350
|
# @!group Persistencia
|
|
251
351
|
|
|
352
|
+
# Guarda el recurso en el servidor remoto vía AMQP (POST o PUT).
|
|
353
|
+
# @return [Boolean]
|
|
252
354
|
def save
|
|
253
355
|
return false unless valid?
|
|
254
356
|
|
|
@@ -268,6 +370,8 @@ module BugBunny
|
|
|
268
370
|
exchange: current_exchange,
|
|
269
371
|
exchange_type: current_exchange_type,
|
|
270
372
|
routing_key: rk,
|
|
373
|
+
exchange_options: @exchange_options,
|
|
374
|
+
queue_options: @queue_options,
|
|
271
375
|
body: wrapped_payload
|
|
272
376
|
)
|
|
273
377
|
|
|
@@ -278,12 +382,24 @@ module BugBunny
|
|
|
278
382
|
false
|
|
279
383
|
end
|
|
280
384
|
|
|
385
|
+
# Elimina el recurso del servidor remoto (DELETE).
|
|
386
|
+
# @return [Boolean]
|
|
281
387
|
def destroy
|
|
282
388
|
return false unless persisted?
|
|
283
389
|
run_callbacks(:destroy) do
|
|
284
390
|
path = "#{self.class.resource_name}/#{id}"
|
|
285
391
|
rk = calculate_routing_key(id)
|
|
286
|
-
|
|
392
|
+
|
|
393
|
+
bug_bunny_client.request(
|
|
394
|
+
path,
|
|
395
|
+
method: :delete,
|
|
396
|
+
exchange: current_exchange,
|
|
397
|
+
exchange_type: current_exchange_type,
|
|
398
|
+
routing_key: rk,
|
|
399
|
+
exchange_options: @exchange_options,
|
|
400
|
+
queue_options: @queue_options
|
|
401
|
+
)
|
|
402
|
+
|
|
287
403
|
self.persisted = false
|
|
288
404
|
end
|
|
289
405
|
true
|
|
@@ -293,20 +409,38 @@ module BugBunny
|
|
|
293
409
|
|
|
294
410
|
private
|
|
295
411
|
|
|
412
|
+
# Maneja la lógica de respuesta para la acción de guardado.
|
|
296
413
|
def handle_save_response(response)
|
|
297
414
|
if response['status'] == 422
|
|
298
415
|
raise BugBunny::UnprocessableEntity.new(response['body']['errors'] || response['body'])
|
|
299
416
|
elsif response['status'] >= 500
|
|
300
|
-
raise BugBunny::InternalServerError
|
|
417
|
+
raise BugBunny::InternalServerError, format_error_message(response['body'])
|
|
301
418
|
elsif response['status'] >= 400
|
|
302
|
-
raise BugBunny::ClientError
|
|
419
|
+
raise BugBunny::ClientError, format_error_message(response['body'])
|
|
303
420
|
end
|
|
421
|
+
|
|
304
422
|
assign_attributes(response['body'])
|
|
305
423
|
self.persisted = true
|
|
306
424
|
clear_changes_information
|
|
307
425
|
true
|
|
308
426
|
end
|
|
309
427
|
|
|
428
|
+
# Formatea el cuerpo de la respuesta de error para que sea legible en las excepciones
|
|
429
|
+
def format_error_message(body)
|
|
430
|
+
return "Unknown Error" if body.nil?
|
|
431
|
+
return body if body.is_a?(String)
|
|
432
|
+
|
|
433
|
+
# Si el worker devolvió un JSON con una key 'error' (nuestra convención en Controller), la priorizamos
|
|
434
|
+
if body.is_a?(Hash) && body['error']
|
|
435
|
+
detail = body['detail'] ? " - #{body['detail']}" : ""
|
|
436
|
+
"#{body['error']}#{detail}"
|
|
437
|
+
else
|
|
438
|
+
# Fallback: Convertir todo el Hash a JSON string para que se vea claro en Sentry/Logs
|
|
439
|
+
body.to_json
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
# Carga errores remotos en el objeto local.
|
|
310
444
|
def load_remote_rabbit_errors(errors_hash)
|
|
311
445
|
return if errors_hash.nil?
|
|
312
446
|
if errors_hash.is_a?(String)
|