bug_bunny 3.0.6 → 3.1.1
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 +28 -0
- data/README.md +204 -148
- data/Rakefile +10 -6
- data/lib/bug_bunny/client.rb +26 -11
- data/lib/bug_bunny/configuration.rb +40 -3
- data/lib/bug_bunny/consumer.rb +56 -29
- data/lib/bug_bunny/controller.rb +137 -93
- data/lib/bug_bunny/exception.rb +4 -0
- data/lib/bug_bunny/producer.rb +45 -29
- data/lib/bug_bunny/request.rb +14 -2
- data/lib/bug_bunny/resource.rb +176 -138
- data/lib/bug_bunny/session.rb +97 -47
- 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 +109 -0
- metadata +47 -8
- data/bin_client.rb +0 -51
- data/bin_suite.rb +0 -106
- data/bin_worker.rb +0 -26
- data/test_controller.rb +0 -49
- data/test_helper.rb +0 -20
- data/test_resource.rb +0 -19
data/lib/bug_bunny/resource.rb
CHANGED
|
@@ -1,71 +1,53 @@
|
|
|
1
|
-
#
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
require 'active_model'
|
|
3
4
|
require 'active_support/core_ext/string/inflections'
|
|
4
5
|
require 'uri'
|
|
6
|
+
require 'set' # Necesario para el tracking manual
|
|
5
7
|
require 'rack/utils'
|
|
6
8
|
|
|
7
9
|
module BugBunny
|
|
8
10
|
# Clase base para modelos remotos que implementan **Active Record over AMQP (RESTful)**.
|
|
9
11
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# compatibilidad con Strong Parameters de Rails.
|
|
15
|
-
#
|
|
16
|
-
# @example Definición de un recurso
|
|
17
|
-
# class User < BugBunny::Resource
|
|
18
|
-
# self.exchange = 'app.topic'
|
|
19
|
-
# self.resource_name = 'users'
|
|
20
|
-
# # Opcional: Personalizar la clave raíz del JSON
|
|
21
|
-
# self.param_key = 'user_data'
|
|
22
|
-
# end
|
|
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`.
|
|
23
16
|
#
|
|
24
|
-
# @
|
|
25
|
-
#
|
|
26
|
-
# user = User.with(routing_key: 'urgent').new(name: 'Gaby')
|
|
27
|
-
# user.save # Enviará a la cola 'urgent' aunque estemos fuera del bloque .with
|
|
17
|
+
# @author Gabriel
|
|
18
|
+
# @since 3.1.0
|
|
28
19
|
class Resource
|
|
29
20
|
include ActiveModel::API
|
|
21
|
+
include ActiveModel::Attributes
|
|
30
22
|
include ActiveModel::Dirty
|
|
31
23
|
include ActiveModel::Validations
|
|
32
24
|
extend ActiveModel::Callbacks
|
|
33
25
|
|
|
34
26
|
define_model_callbacks :save, :create, :update, :destroy
|
|
35
27
|
|
|
36
|
-
# @return [HashWithIndifferentAccess] Contenedor de los atributos remotos (JSON crudo).
|
|
37
28
|
attr_reader :remote_attributes
|
|
38
|
-
|
|
39
|
-
# @return [Boolean] Indica si el objeto ha sido guardado en el servicio remoto.
|
|
40
29
|
attr_accessor :persisted
|
|
30
|
+
attr_accessor :routing_key, :exchange, :exchange_type
|
|
41
31
|
|
|
42
|
-
# @return [
|
|
43
|
-
attr_accessor :
|
|
44
|
-
|
|
45
|
-
# @return [String, nil] Exchange capturado en el momento de la instanciación.
|
|
46
|
-
attr_accessor :exchange
|
|
47
|
-
|
|
48
|
-
# @return [String, nil] Tipo de Exchange capturado en el momento de la instanciación.
|
|
49
|
-
attr_accessor :exchange_type
|
|
32
|
+
# @return [Hash] Opciones específicas de instancia para exchange y queue.
|
|
33
|
+
attr_accessor :exchange_options, :queue_options
|
|
50
34
|
|
|
51
35
|
class << self
|
|
52
|
-
# Configuración heredable
|
|
53
36
|
attr_writer :connection_pool, :exchange, :exchange_type, :resource_name, :routing_key, :param_key
|
|
54
37
|
|
|
55
|
-
#
|
|
56
|
-
|
|
57
|
-
def thread_config(key)
|
|
58
|
-
Thread.current["bb_#{object_id}_#{key}"]
|
|
59
|
-
end
|
|
38
|
+
# @!group Configuración de Infraestructura Específica
|
|
39
|
+
attr_writer :exchange_options, :queue_options
|
|
60
40
|
|
|
61
|
-
# Resuelve la configuración buscando en: 1. Thread (Scope), 2. Clase, 3. Herencia.
|
|
62
41
|
# @api private
|
|
42
|
+
def thread_config(key); Thread.current["bb_#{object_id}_#{key}"]; end
|
|
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]
|
|
63
48
|
def resolve_config(key, instance_var)
|
|
64
|
-
# 1. Prioridad: Contexto de hilo (.with)
|
|
65
49
|
val = thread_config(key)
|
|
66
50
|
return val if val
|
|
67
|
-
|
|
68
|
-
# 2. Prioridad: Jerarquía de clases
|
|
69
51
|
target = self
|
|
70
52
|
while target <= BugBunny::Resource
|
|
71
53
|
value = target.instance_variable_get(instance_var)
|
|
@@ -75,32 +57,32 @@ module BugBunny
|
|
|
75
57
|
nil
|
|
76
58
|
end
|
|
77
59
|
|
|
78
|
-
# @return [ConnectionPool]
|
|
60
|
+
# @return [ConnectionPool, nil]
|
|
79
61
|
def connection_pool; resolve_config(:pool, :@connection_pool); end
|
|
80
62
|
|
|
81
|
-
# @return [String]
|
|
82
|
-
|
|
83
|
-
def current_exchange; resolve_config(:exchange, :@exchange) || raise(ArgumentError, "Exchange not defined"); end
|
|
63
|
+
# @return [String] Nombre del exchange actual.
|
|
64
|
+
def current_exchange; resolve_config(:exchange, :@exchange) || raise(ArgumentError, "Exchange not defined for #{name}"); end
|
|
84
65
|
|
|
85
|
-
# @return [String]
|
|
66
|
+
# @return [String] Tipo de exchange ('direct', 'topic', 'fanout').
|
|
86
67
|
def current_exchange_type; resolve_config(:exchange_type, :@exchange_type) || 'direct'; end
|
|
87
68
|
|
|
88
|
-
# @return [
|
|
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.
|
|
89
76
|
def resource_name
|
|
90
77
|
resolve_config(:resource_name, :@resource_name) || name.demodulize.underscore.pluralize
|
|
91
78
|
end
|
|
92
79
|
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
# Por defecto utiliza `model_name.element`, lo que elimina los namespaces.
|
|
96
|
-
# Ej: `Manager::Service` -> `'service'`.
|
|
97
|
-
#
|
|
98
|
-
# @return [String] La clave paramétrica.
|
|
80
|
+
# @return [String] Clave raíz para envolver el payload en las peticiones.
|
|
99
81
|
def param_key
|
|
100
82
|
resolve_config(:param_key, :@param_key) || model_name.element
|
|
101
83
|
end
|
|
102
84
|
|
|
103
|
-
#
|
|
85
|
+
# @api private
|
|
104
86
|
def client_middleware(&block)
|
|
105
87
|
@client_middleware_stack ||= []
|
|
106
88
|
@client_middleware_stack << block
|
|
@@ -118,32 +100,42 @@ module BugBunny
|
|
|
118
100
|
stack
|
|
119
101
|
end
|
|
120
102
|
|
|
121
|
-
# Instancia
|
|
103
|
+
# Instancia el cliente inyectando los middlewares configurados.
|
|
122
104
|
# @return [BugBunny::Client]
|
|
123
105
|
def bug_bunny_client
|
|
124
106
|
pool = connection_pool
|
|
125
107
|
raise BugBunny::Error, "Connection pool missing for #{name}" unless pool
|
|
126
|
-
|
|
127
108
|
BugBunny::Client.new(pool: pool) do |conn|
|
|
128
109
|
resolve_middleware_stack.each { |block| block.call(conn) }
|
|
129
110
|
end
|
|
130
111
|
end
|
|
131
112
|
|
|
132
|
-
#
|
|
133
|
-
# Útil para cambiar de exchange o routing_key para una operación específica.
|
|
113
|
+
# Permite configurar dinámicamente el contexto AMQP para una operación.
|
|
134
114
|
#
|
|
135
|
-
# @
|
|
136
|
-
#
|
|
137
|
-
|
|
138
|
-
|
|
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
|
+
}
|
|
139
130
|
old_values = {}
|
|
140
131
|
keys.each { |k, v| old_values[k] = Thread.current[v] }
|
|
141
132
|
|
|
142
|
-
# Seteamos valores temporales
|
|
143
133
|
Thread.current[keys[:exchange]] = exchange if exchange
|
|
144
134
|
Thread.current[keys[:exchange_type]] = exchange_type if exchange_type
|
|
145
135
|
Thread.current[keys[:pool]] = pool if pool
|
|
146
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
|
|
147
139
|
|
|
148
140
|
if block_given?
|
|
149
141
|
begin; yield; ensure; keys.each { |k, v| Thread.current[v] = old_values[k] }; end
|
|
@@ -152,39 +144,42 @@ module BugBunny
|
|
|
152
144
|
end
|
|
153
145
|
end
|
|
154
146
|
|
|
155
|
-
# Proxy para
|
|
147
|
+
# Proxy para el encadenamiento del método `.with`.
|
|
156
148
|
class ScopeProxy < BasicObject
|
|
157
149
|
def initialize(target, keys, old_values); @target = target; @keys = keys; @old_values = old_values; end
|
|
158
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
|
|
159
151
|
end
|
|
160
152
|
|
|
161
|
-
# Calcula la
|
|
153
|
+
# Calcula la routing key final.
|
|
154
|
+
# @param id [String, nil] ID del recurso.
|
|
162
155
|
# @return [String]
|
|
163
156
|
def calculate_routing_key(id = nil)
|
|
164
|
-
# 1. Contexto .with
|
|
165
157
|
manual_rk = thread_config(:routing_key)
|
|
166
158
|
return manual_rk if manual_rk
|
|
167
|
-
|
|
168
|
-
# 2. Configuración estática
|
|
169
159
|
static_rk = resolve_config(:routing_key, :@routing_key)
|
|
170
160
|
return static_rk if static_rk.present?
|
|
171
|
-
|
|
172
|
-
# 3. Default: Resource name
|
|
173
161
|
resource_name
|
|
174
162
|
end
|
|
175
163
|
|
|
176
|
-
# @!group Acciones CRUD RESTful
|
|
164
|
+
# @!group Acciones CRUD RESTful
|
|
177
165
|
|
|
178
|
-
#
|
|
179
|
-
#
|
|
166
|
+
# Realiza una búsqueda filtrada (GET).
|
|
167
|
+
# @param filters [Hash]
|
|
168
|
+
# @return [Array<BugBunny::Resource>]
|
|
180
169
|
def where(filters = {})
|
|
181
170
|
rk = calculate_routing_key
|
|
182
171
|
path = resource_name
|
|
183
|
-
|
|
184
|
-
# Usamos Rack para serializar anidamiento (q[service]=val)
|
|
185
172
|
path += "?#{Rack::Utils.build_nested_query(filters)}" if filters.present?
|
|
186
173
|
|
|
187
|
-
response = bug_bunny_client.request(
|
|
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
|
+
)
|
|
188
183
|
|
|
189
184
|
return [] unless response['body'].is_a?(Array)
|
|
190
185
|
response['body'].map do |attrs|
|
|
@@ -195,28 +190,38 @@ module BugBunny
|
|
|
195
190
|
end
|
|
196
191
|
end
|
|
197
192
|
|
|
193
|
+
# Devuelve todos los registros.
|
|
194
|
+
# @return [Array<BugBunny::Resource>]
|
|
198
195
|
def all; where({}); end
|
|
199
196
|
|
|
200
|
-
# Busca un
|
|
201
|
-
#
|
|
197
|
+
# Busca un registro por ID (GET).
|
|
198
|
+
# @param id [String, Integer]
|
|
199
|
+
# @return [BugBunny::Resource, nil]
|
|
202
200
|
def find(id)
|
|
203
201
|
rk = calculate_routing_key(id)
|
|
204
202
|
path = "#{resource_name}/#{id}"
|
|
205
203
|
|
|
206
|
-
response = bug_bunny_client.request(
|
|
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
|
+
)
|
|
207
213
|
|
|
208
214
|
return nil if response.nil? || response['status'] == 404
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
return nil unless attributes.is_a?(Hash)
|
|
212
|
-
|
|
213
|
-
instance = new(attributes)
|
|
215
|
+
return nil unless response['body'].is_a?(Hash)
|
|
216
|
+
instance = new(response['body'])
|
|
214
217
|
instance.persisted = true
|
|
215
218
|
instance.send(:clear_changes_information)
|
|
216
219
|
instance
|
|
217
220
|
end
|
|
218
221
|
|
|
219
|
-
# Crea
|
|
222
|
+
# Crea una nueva instancia y la persiste.
|
|
223
|
+
# @param payload [Hash]
|
|
224
|
+
# @return [BugBunny::Resource]
|
|
220
225
|
def create(payload)
|
|
221
226
|
instance = new(payload)
|
|
222
227
|
instance.save
|
|
@@ -226,70 +231,94 @@ module BugBunny
|
|
|
226
231
|
|
|
227
232
|
# @!group Instancia
|
|
228
233
|
|
|
229
|
-
# Inicializa
|
|
230
|
-
#
|
|
231
|
-
# **IMPORTANTE:** Captura la configuración del contexto actual (`.with`)
|
|
232
|
-
# y la guarda en la instancia. Esto permite que objetos creados dentro de un bloque `with`
|
|
233
|
-
# mantengan esa configuración (routing_key, exchange) durante todo su ciclo de vida,
|
|
234
|
-
# incluso si `save` se llama fuera del bloque.
|
|
235
|
-
#
|
|
236
|
-
# @param attributes [Hash] Atributos iniciales.
|
|
234
|
+
# Inicializa el recurso.
|
|
235
|
+
# @param attributes [Hash]
|
|
237
236
|
def initialize(attributes = {})
|
|
238
237
|
@remote_attributes = {}.with_indifferent_access
|
|
238
|
+
@dynamic_changes = Set.new # Rastreo manual para atributos dinámicos
|
|
239
239
|
@persisted = false
|
|
240
240
|
|
|
241
|
-
#
|
|
241
|
+
# Contexto de infraestructura
|
|
242
242
|
@routing_key = self.class.thread_config(:routing_key)
|
|
243
243
|
@exchange = self.class.thread_config(:exchange)
|
|
244
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
|
|
245
247
|
|
|
246
|
-
assign_attributes(attributes)
|
|
247
248
|
super()
|
|
249
|
+
assign_attributes(attributes)
|
|
248
250
|
end
|
|
249
251
|
|
|
250
|
-
#
|
|
251
|
-
def
|
|
252
|
-
|
|
253
|
-
|
|
252
|
+
# Limpia tanto el rastreo de ActiveModel como nuestro rastreo dinámico.
|
|
253
|
+
def clear_changes_information
|
|
254
|
+
super
|
|
255
|
+
@dynamic_changes.clear
|
|
254
256
|
end
|
|
255
257
|
|
|
256
|
-
#
|
|
257
|
-
|
|
258
|
-
|
|
258
|
+
# Serialización combinada.
|
|
259
|
+
# @return [Hash]
|
|
260
|
+
def attributes_for_serialization
|
|
261
|
+
@remote_attributes.merge(attributes)
|
|
259
262
|
end
|
|
260
263
|
|
|
261
|
-
#
|
|
262
|
-
def
|
|
263
|
-
|
|
264
|
-
|
|
264
|
+
# @return [String]
|
|
265
|
+
def calculate_routing_key(id=nil); @routing_key || self.class.calculate_routing_key(id); end
|
|
266
|
+
|
|
267
|
+
# @return [String]
|
|
268
|
+
def current_exchange; @exchange || self.class.current_exchange; end
|
|
269
|
+
|
|
270
|
+
# @return [String]
|
|
271
|
+
def current_exchange_type; @exchange_type || self.class.current_exchange_type; end
|
|
265
272
|
|
|
273
|
+
# @return [BugBunny::Client]
|
|
266
274
|
def bug_bunny_client; self.class.bug_bunny_client; end
|
|
267
275
|
|
|
276
|
+
# @return [Boolean]
|
|
268
277
|
def persisted?; !!@persisted; end
|
|
269
278
|
|
|
279
|
+
# Asignación masiva de atributos.
|
|
280
|
+
# @param new_attributes [Hash]
|
|
270
281
|
def assign_attributes(new_attributes)
|
|
271
282
|
return if new_attributes.nil?
|
|
272
283
|
new_attributes.each { |k, v| public_send("#{k}=", v) }
|
|
273
284
|
end
|
|
274
285
|
|
|
286
|
+
# Actualiza y guarda.
|
|
287
|
+
# @param attributes [Hash]
|
|
288
|
+
# @return [Boolean]
|
|
275
289
|
def update(attributes)
|
|
276
290
|
assign_attributes(attributes)
|
|
277
291
|
save
|
|
278
292
|
end
|
|
279
293
|
|
|
280
|
-
# Retorna
|
|
294
|
+
# Retorna el hash combinado de cambios (Tipados + Dinámicos).
|
|
295
|
+
# @return [Hash]
|
|
281
296
|
def changes_to_send
|
|
282
|
-
|
|
283
|
-
|
|
297
|
+
# 1. Cambios de ActiveModel (Tipados)
|
|
298
|
+
payload = changes.transform_values(&:last)
|
|
299
|
+
|
|
300
|
+
# 2. Cambios Dinámicos (Manuales)
|
|
301
|
+
@dynamic_changes.each do |key|
|
|
302
|
+
payload[key] = @remote_attributes[key]
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
return payload unless payload.empty?
|
|
306
|
+
|
|
307
|
+
# Fallback: Si no hay cambios detectados, enviamos todo (útil para create)
|
|
308
|
+
attributes_for_serialization.except('id', 'ID', 'Id', '_id')
|
|
284
309
|
end
|
|
285
310
|
|
|
286
|
-
#
|
|
311
|
+
# Intercepta asignaciones dinámicas y las marca como sucias.
|
|
287
312
|
def method_missing(method_name, *args, &block)
|
|
288
313
|
attribute_name = method_name.to_s
|
|
289
314
|
if attribute_name.end_with?('=')
|
|
290
315
|
key = attribute_name.chop
|
|
291
316
|
val = args.first
|
|
292
|
-
|
|
317
|
+
|
|
318
|
+
if @remote_attributes[key] != val
|
|
319
|
+
@dynamic_changes << key
|
|
320
|
+
end
|
|
321
|
+
|
|
293
322
|
@remote_attributes[key] = val
|
|
294
323
|
else
|
|
295
324
|
@remote_attributes.key?(attribute_name) ? @remote_attributes[attribute_name] : super
|
|
@@ -300,51 +329,51 @@ module BugBunny
|
|
|
300
329
|
@remote_attributes.key?(method_name.to_s.sub(/=$/, '')) || super
|
|
301
330
|
end
|
|
302
331
|
|
|
332
|
+
# @return [Object] Valor del ID buscando en múltiples nomenclaturas.
|
|
303
333
|
def id
|
|
304
|
-
@remote_attributes['id'] || @remote_attributes['ID'] || @remote_attributes['Id'] || @remote_attributes['_id']
|
|
334
|
+
attributes['id'] || @remote_attributes['id'] || @remote_attributes['ID'] || @remote_attributes['Id'] || @remote_attributes['_id']
|
|
305
335
|
end
|
|
306
336
|
|
|
307
337
|
def id=(value)
|
|
308
|
-
|
|
338
|
+
if self.class.attribute_names.include?('id')
|
|
339
|
+
super(value)
|
|
340
|
+
else
|
|
341
|
+
@remote_attributes['id'] = value
|
|
342
|
+
end
|
|
309
343
|
end
|
|
310
344
|
|
|
311
345
|
def read_attribute_for_validation(attr)
|
|
312
|
-
|
|
346
|
+
attr_s = attr.to_s
|
|
347
|
+
self.class.attribute_names.include?(attr_s) ? attribute(attr_s) : @remote_attributes[attr_s]
|
|
313
348
|
end
|
|
314
349
|
|
|
315
|
-
# @!group Persistencia
|
|
350
|
+
# @!group Persistencia
|
|
316
351
|
|
|
317
|
-
# Guarda el
|
|
318
|
-
#
|
|
319
|
-
#
|
|
320
|
-
# **AUTOMÁTICO:** Envuelve los parámetros en la clave del modelo (`param_key`).
|
|
321
|
-
# Ej: Manager::Service -> "service". Esto facilita `params.require(:service)`.
|
|
322
|
-
#
|
|
323
|
-
# @return [Boolean] true si se guardó correctamente.
|
|
352
|
+
# Guarda el recurso en el servidor remoto vía AMQP (POST o PUT).
|
|
353
|
+
# @return [Boolean]
|
|
324
354
|
def save
|
|
325
355
|
return false unless valid?
|
|
326
356
|
|
|
327
357
|
run_callbacks(:save) do
|
|
328
358
|
is_new = !persisted?
|
|
329
359
|
rk = calculate_routing_key(id)
|
|
330
|
-
|
|
331
|
-
# 1. Obtenemos el payload plano (atributos modificados)
|
|
332
360
|
flat_payload = changes_to_send
|
|
333
|
-
|
|
334
|
-
# 2. Wrappeamos automáticamente en la clave del modelo
|
|
335
361
|
key = self.class.param_key
|
|
336
362
|
wrapped_payload = { key => flat_payload }
|
|
337
363
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
364
|
+
path = is_new ? self.class.resource_name : "#{self.class.resource_name}/#{id}"
|
|
365
|
+
method = is_new ? :post : :put
|
|
366
|
+
|
|
367
|
+
response = bug_bunny_client.request(
|
|
368
|
+
path,
|
|
369
|
+
method: method,
|
|
370
|
+
exchange: current_exchange,
|
|
371
|
+
exchange_type: current_exchange_type,
|
|
372
|
+
routing_key: rk,
|
|
373
|
+
exchange_options: @exchange_options,
|
|
374
|
+
queue_options: @queue_options,
|
|
375
|
+
body: wrapped_payload
|
|
376
|
+
)
|
|
348
377
|
|
|
349
378
|
handle_save_response(response)
|
|
350
379
|
end
|
|
@@ -353,16 +382,23 @@ module BugBunny
|
|
|
353
382
|
false
|
|
354
383
|
end
|
|
355
384
|
|
|
356
|
-
# Elimina el
|
|
357
|
-
#
|
|
385
|
+
# Elimina el recurso del servidor remoto (DELETE).
|
|
386
|
+
# @return [Boolean]
|
|
358
387
|
def destroy
|
|
359
388
|
return false unless persisted?
|
|
360
|
-
|
|
361
389
|
run_callbacks(:destroy) do
|
|
362
390
|
path = "#{self.class.resource_name}/#{id}"
|
|
363
391
|
rk = calculate_routing_key(id)
|
|
364
392
|
|
|
365
|
-
bug_bunny_client.request(
|
|
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
|
+
)
|
|
366
402
|
|
|
367
403
|
self.persisted = false
|
|
368
404
|
end
|
|
@@ -373,6 +409,7 @@ module BugBunny
|
|
|
373
409
|
|
|
374
410
|
private
|
|
375
411
|
|
|
412
|
+
# Maneja la lógica de respuesta para la acción de guardado.
|
|
376
413
|
def handle_save_response(response)
|
|
377
414
|
if response['status'] == 422
|
|
378
415
|
raise BugBunny::UnprocessableEntity.new(response['body']['errors'] || response['body'])
|
|
@@ -388,6 +425,7 @@ module BugBunny
|
|
|
388
425
|
true
|
|
389
426
|
end
|
|
390
427
|
|
|
428
|
+
# Carga errores remotos en el objeto local.
|
|
391
429
|
def load_remote_rabbit_errors(errors_hash)
|
|
392
430
|
return if errors_hash.nil?
|
|
393
431
|
if errors_hash.is_a?(String)
|