bug_bunny 1.0.1 → 2.0.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/README.md +18 -61
- data/Rakefile +5 -1
- data/lib/bug_bunny/config.rb +2 -2
- data/lib/bug_bunny/controller.rb +80 -7
- data/lib/bug_bunny/exception.rb +14 -66
- data/lib/bug_bunny/publisher.rb +115 -0
- data/lib/bug_bunny/rabbit.rb +283 -83
- data/lib/bug_bunny/resource.rb +226 -0
- data/lib/bug_bunny/version.rb +1 -1
- data/lib/bug_bunny.rb +7 -16
- metadata +8 -13
- data/Gemfile +0 -8
- data/lib/bug_bunny/adapter.rb +0 -347
- data/lib/bug_bunny/helpers.rb +0 -36
- data/lib/bug_bunny/message.rb +0 -161
- data/lib/bug_bunny/queue.rb +0 -23
- data/lib/bug_bunny/railtie.rb +0 -124
- data/lib/bug_bunny/response.rb +0 -15
- data/lib/bug_bunny/security.rb +0 -19
data/lib/bug_bunny/rabbit.rb
CHANGED
|
@@ -1,108 +1,308 @@
|
|
|
1
|
+
# host: Especifica la dirección de red (hostname o IP) donde se está ejecutando el servidor RabbitMQ.
|
|
2
|
+
# username: El nombre de usuario que se utiliza para la autenticación.
|
|
3
|
+
# password: La contraseña para la autenticación.
|
|
4
|
+
# vhost: Define el Virtual Host (VHost) al que se conectará la aplicación. Un VHost actúa como un namespace virtual dentro del broker, aislando entornos y recursos.
|
|
5
|
+
# logger: Indica a Bunny que use el sistema de logging estándar de Rails, integrando los mensajes del cliente AMQP con el resto de los logs de tu aplicación.
|
|
6
|
+
#
|
|
7
|
+
# Resiliencia y Recuperación Automática
|
|
8
|
+
#
|
|
9
|
+
# Estos parámetros son fundamentales para manejar fallos de red y garantizar que la aplicación se recupere sin intervención manual.
|
|
10
|
+
# automatically_recover: Indica al cliente Bunny que debe intentar automáticamente reestablecer la conexión y todos los recursos asociados (canales, colas, exchanges) si la conexión se pierde debido a un fallo de red o un reinicio del broker. Nota: Este parámetro puede entrar en conflicto con un bucle de retry manual).
|
|
11
|
+
# network_recovery_interval: El tiempo que Bunny esperará entre intentos consecutivos de reconexión de red.
|
|
12
|
+
# heartbeat: El intervalo de tiempo (en segundos) en el que el cliente y el servidor deben enviarse un pequeño paquete ("latido"). Si no se recibe un heartbeat durante dos intervalos consecutivos, se asume que la conexión ha muerto (generalmente por un fallo de red o un proceso colgado), lo que dispara el mecanismo de recuperación.
|
|
13
|
+
#
|
|
14
|
+
# Tiempos de Espera (Timeouts)
|
|
15
|
+
#
|
|
16
|
+
# Estos parámetros previenen que la aplicación se bloquee indefinidamente esperando una respuesta del servidor.
|
|
17
|
+
# connection_timeout: Tiempo máximo (en segundos) que Bunny esperará para establecer la conexión TCP inicial con el servidor RabbitMQ.
|
|
18
|
+
# read_timeout: Tiempo máximo (en segundos) que la conexión esperará para leer datos del socket. Si el servidor se queda en silencio por más de 30 segundos, el socket se cerrará.
|
|
19
|
+
# write_timeout: Tiempo máximo (en segundos) que la conexión esperará para escribir datos en el socket. Útil para manejar escenarios donde la red es lenta o está congestionada.
|
|
20
|
+
# continuation_timeout: Es un timeout interno de protocolo AMQP (dado en milisegundos). Define cuánto tiempo esperará el cliente para que el servidor responda a una operación que requiere múltiples frames o pasos (como una transacción o una confirmación compleja). En este caso, son 15 segundos.
|
|
1
21
|
module BugBunny
|
|
2
22
|
class Rabbit
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
@
|
|
21
|
-
@
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
include ActiveModel::Model
|
|
24
|
+
include ActiveModel::Attributes
|
|
25
|
+
|
|
26
|
+
DEFAULT_EXCHANGE_OPTIONS = { durable: false, auto_delete: false }.freeze
|
|
27
|
+
|
|
28
|
+
# DEFAULT_MAX_PRIORITY = 10
|
|
29
|
+
|
|
30
|
+
DEFAULT_QUEUE_OPTIONS = {
|
|
31
|
+
exclusive: false,
|
|
32
|
+
durable: false,
|
|
33
|
+
auto_delete: true,
|
|
34
|
+
# arguments: { 'x-max-priority' => DEFAULT_MAX_PRIORITY }
|
|
35
|
+
}.freeze
|
|
36
|
+
|
|
37
|
+
attr_accessor :connection, :queue, :exchange
|
|
38
|
+
|
|
39
|
+
def channel
|
|
40
|
+
@channel_mutex ||= Mutex.new
|
|
41
|
+
return @channel if @channel&.open?
|
|
42
|
+
|
|
43
|
+
@channel_mutex.synchronize do
|
|
44
|
+
return @channel if @channel&.open?
|
|
45
|
+
|
|
46
|
+
@channel = connection.create_channel
|
|
47
|
+
@channel.confirm_select
|
|
48
|
+
@channel.prefetch(RABBIT_CHANNEL_PREFETCH) # Limita mensajes concurrentes por consumidor
|
|
49
|
+
@channel
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def build_exchange(name: nil, type: 'direct', opts: {})
|
|
54
|
+
return @exchange if defined?(@exchange)
|
|
55
|
+
|
|
56
|
+
exchange_options = DEFAULT_EXCHANGE_OPTIONS.merge(opts.compact)
|
|
57
|
+
|
|
58
|
+
if name.blank?
|
|
59
|
+
@exchange = channel.default_exchange
|
|
60
|
+
return @exchange
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
Rails.logger.info("ExchangeName: #{name}, ExchangeType: #{type}, opts: #{opts}")
|
|
64
|
+
|
|
65
|
+
@exchange = case type.to_sym
|
|
66
|
+
when :topic
|
|
67
|
+
channel.topic(name, exchange_options)
|
|
68
|
+
when :direct
|
|
69
|
+
channel.direct(name, exchange_options)
|
|
70
|
+
when :fanout
|
|
71
|
+
channel.fanout(name, exchange_options)
|
|
72
|
+
when :headers
|
|
73
|
+
channel.headers(name, exchange_options)
|
|
74
|
+
end
|
|
25
75
|
end
|
|
26
76
|
|
|
27
|
-
def
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
77
|
+
def default_publish_options
|
|
78
|
+
@default_publish_opts ||= {
|
|
79
|
+
persistent: false,
|
|
80
|
+
app_id: Rails.application.class.module_parent_name
|
|
81
|
+
}.freeze
|
|
82
|
+
|
|
83
|
+
# Solo generamos valores dinámicos por llamada
|
|
84
|
+
@default_publish_opts.merge(
|
|
85
|
+
timestamp: Time.current.to_i,
|
|
86
|
+
correlation_id: SecureRandom.uuid
|
|
87
|
+
)
|
|
34
88
|
end
|
|
35
89
|
|
|
36
|
-
def
|
|
37
|
-
|
|
90
|
+
def build_queue(name: '', opts: {})
|
|
91
|
+
name = name.to_s
|
|
92
|
+
queue_options = DEFAULT_QUEUE_OPTIONS.merge(opts.compact)
|
|
93
|
+
Rails.logger.info("QueueName: #{name}, opts: #{queue_options}")
|
|
94
|
+
@queue = channel.queue(name, queue_options)
|
|
38
95
|
end
|
|
39
96
|
|
|
40
|
-
def
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
97
|
+
def publish!(msg, opts)
|
|
98
|
+
options = default_publish_options.merge(opts.compact)
|
|
99
|
+
|
|
100
|
+
msg = msg.instance_of?(Hash) ? msg.to_json : msg.to_s
|
|
101
|
+
|
|
102
|
+
Rails.logger.info("Message: #{msg}")
|
|
103
|
+
Rails.logger.info("Options: #{options}")
|
|
104
|
+
|
|
105
|
+
exchange.publish(msg, options)
|
|
106
|
+
# channel.wait_for_confirms # Esto solo confirma que el mensaje llego el exchange
|
|
107
|
+
rescue Bunny::Exception => e
|
|
108
|
+
Rails.logger.error(e)
|
|
109
|
+
raise BugBunny::PublishError, e
|
|
45
110
|
end
|
|
46
111
|
|
|
47
|
-
def
|
|
48
|
-
|
|
112
|
+
def publish_and_consume!(msg, opts)
|
|
113
|
+
options = default_publish_options.merge(opts.compact)
|
|
114
|
+
|
|
115
|
+
response_latch = Concurrent::CountDownLatch.new(1)
|
|
116
|
+
response = nil
|
|
117
|
+
|
|
118
|
+
reply_queue = channel.queue('', exclusive: true, durable: false, auto_delete: true)
|
|
119
|
+
options[:reply_to] = reply_queue.name
|
|
120
|
+
|
|
121
|
+
subscription = reply_queue.subscribe(manual_ack: true, block: false) do |delivery_info, properties, body|
|
|
122
|
+
Rails.logger.debug("CONSUMER DeliveryInfo: #{delivery_info}")
|
|
123
|
+
Rails.logger.debug("CONSUMER Properties: #{properties}")
|
|
124
|
+
Rails.logger.debug("CONSUMER Body: #{body}")
|
|
125
|
+
if properties.correlation_id == options[:correlation_id]
|
|
126
|
+
response = ActiveSupport::JSON.decode(body).deep_symbolize_keys.with_indifferent_access
|
|
127
|
+
channel.ack(delivery_info.delivery_tag)
|
|
128
|
+
response_latch.count_down
|
|
129
|
+
else
|
|
130
|
+
Rails.logger.debug('Correlation_id not match')
|
|
131
|
+
# Si el correlation_id no coincide, rechazamos el mensaje para que RabbitMQ lo maneje
|
|
132
|
+
channel.reject(delivery_info.delivery_tag, false)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
Rails.logger.debug("PUBLISHER Message: #{msg}")
|
|
137
|
+
Rails.logger.debug("PUBLISHER Options: #{options}")
|
|
138
|
+
publish!(msg, options)
|
|
139
|
+
|
|
140
|
+
if response_latch.wait(RABBIT_READ_TIMEOUT)
|
|
141
|
+
subscription.cancel
|
|
142
|
+
build_response(status: response[:status], body: response[:body])
|
|
143
|
+
else
|
|
144
|
+
raise "Timeout: No response received within #{RABBIT_READ_TIMEOUT} seconds."
|
|
145
|
+
end
|
|
146
|
+
rescue BugBunny::ResponseError::Base => e
|
|
147
|
+
subscription&.cancel
|
|
148
|
+
raise e
|
|
149
|
+
rescue RuntimeError => e
|
|
150
|
+
subscription&.cancel
|
|
151
|
+
Rails.logger.error("[Rabbit] Error in publish_and_consume: #{e.class} - <#{e.message}>")
|
|
152
|
+
raise(BugBunny::ResponseError::RequestTimeout, e.message) if e.message.include?('Timeout')
|
|
153
|
+
|
|
154
|
+
raise BugBunny::ResponseError::InternalServerError, e.message
|
|
155
|
+
rescue StandardError => e
|
|
156
|
+
subscription&.cancel
|
|
157
|
+
Rails.logger.error("[Rabbit] Error in publish_and_consume: #{e.class} - <#{e.message}>")
|
|
158
|
+
raise e
|
|
49
159
|
end
|
|
50
160
|
|
|
51
|
-
def
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
161
|
+
def parse_route(route)
|
|
162
|
+
# De momento no resuelve anidado
|
|
163
|
+
segments = route.split('/')
|
|
164
|
+
controller_name = segments[0]
|
|
165
|
+
action_name = 'index'
|
|
166
|
+
id = nil
|
|
167
|
+
|
|
168
|
+
case segments.length
|
|
169
|
+
when 2
|
|
170
|
+
# Patrón: controller/action (Ej: 'secrets/index', 'swarm/info')
|
|
171
|
+
action_name = segments[1]
|
|
172
|
+
when 3
|
|
173
|
+
# Patrón: controller/id/action (Ej: 'secrets/123/update', 'services/999/destroy')
|
|
174
|
+
id = segments[1]
|
|
175
|
+
action_name = segments[2]
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
{ controller: controller_name, action: action_name, id: id }
|
|
57
179
|
end
|
|
58
180
|
|
|
59
|
-
def
|
|
60
|
-
(
|
|
61
|
-
(
|
|
181
|
+
def consume!
|
|
182
|
+
queue.subscribe(manual_ack: true, block: true) do |delivery_info, properties, body|
|
|
183
|
+
Rails.logger.debug("DeliveryInfo: #{delivery_info}")
|
|
184
|
+
Rails.logger.debug("Properties: #{properties}")
|
|
185
|
+
Rails.logger.debug("Body: #{body}")
|
|
186
|
+
|
|
187
|
+
raise StandardError, 'Undefined properties.type' if properties.type.blank?
|
|
188
|
+
|
|
189
|
+
route = parse_route(properties.type)
|
|
190
|
+
|
|
191
|
+
headers = {
|
|
192
|
+
type: properties.type,
|
|
193
|
+
controller: route[:controller],
|
|
194
|
+
action: route[:action],
|
|
195
|
+
id: route[:id],
|
|
196
|
+
content_type: properties.content_type,
|
|
197
|
+
content_encoding: properties.content_encoding,
|
|
198
|
+
correlation_id: properties.correlation_id
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
controller = "rabbit/controllers/#{route[:controller]}".camelize.constantize
|
|
202
|
+
response_payload = controller.call(headers: headers, body: body)
|
|
203
|
+
|
|
204
|
+
Rails.logger.debug("Response: #{response_payload}")
|
|
205
|
+
|
|
206
|
+
if properties.reply_to.present?
|
|
207
|
+
Rails.logger.info("Sending response to #{properties.reply_to}")
|
|
208
|
+
|
|
209
|
+
# Publicar la respuesta directamente a la cola de respuesta
|
|
210
|
+
# No se necesita un exchange, se publica a la cola por su nombre
|
|
211
|
+
channel.default_exchange.publish(
|
|
212
|
+
response_payload.to_json,
|
|
213
|
+
routing_key: properties.reply_to,
|
|
214
|
+
correlation_id: properties.correlation_id
|
|
215
|
+
)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
channel.ack(delivery_info.delivery_tag)
|
|
219
|
+
rescue NoMethodError => e # action controller no exist
|
|
220
|
+
Rails.logger.error(e)
|
|
221
|
+
channel.reject(delivery_info.delivery_tag, false)
|
|
222
|
+
rescue NameError => e # Controller no exist
|
|
223
|
+
Rails.logger.error(e)
|
|
224
|
+
channel.reject(delivery_info.delivery_tag, false)
|
|
225
|
+
rescue StandardError => e
|
|
226
|
+
Rails.logger.error("Error processing message: #{e.message} (#{e.class})")
|
|
227
|
+
# Reject the message and do NOT re-queue it immediately.
|
|
228
|
+
channel.reject(delivery_info.delivery_tag, false)
|
|
229
|
+
end
|
|
62
230
|
end
|
|
63
231
|
|
|
64
|
-
|
|
65
|
-
|
|
232
|
+
# El success y el error es para tener compatibilidad con el bug_bunny
|
|
233
|
+
def build_response(status:, body:)
|
|
234
|
+
case status
|
|
235
|
+
when 'success' then body # Old compatibility
|
|
236
|
+
when 'error' then raise BugBunny::ResponseError::InternalServerError, body # Old compatibility
|
|
237
|
+
when 200, 201 then body
|
|
238
|
+
when 204 then nil
|
|
239
|
+
when 400 then raise BugBunny::ResponseError::BadRequest, body.to_json
|
|
240
|
+
when 404 then raise BugBunny::ResponseError::NotFound
|
|
241
|
+
when 406 then raise BugBunny::ResponseError::NotAcceptable
|
|
242
|
+
when 422 then raise BugBunny::ResponseError::UnprocessableEntity, body.to_json
|
|
243
|
+
when 500 then raise BugBunny::ResponseError::InternalServerError, body.to_json
|
|
244
|
+
else
|
|
245
|
+
raise BugBunny::ResponseError::Base, body.to_json
|
|
246
|
+
end
|
|
66
247
|
end
|
|
67
248
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
bunny_logger.level = BugBunny.configuration.log_level || ::Logger::INFO
|
|
89
|
-
|
|
90
|
-
options.merge!(
|
|
91
|
-
heartbeat_interval: 20, # 20.seconds per connection
|
|
92
|
-
logger: bunny_logger,
|
|
93
|
-
# Override bunny client_propierties
|
|
94
|
-
client_properties: { product: identifier, platform: '' }
|
|
95
|
-
)
|
|
249
|
+
def self.run_consumer(connection:, exchange:, exchange_type:, queue_name:, routing_key:, queue_opts: {})
|
|
250
|
+
app = new(connection: connection)
|
|
251
|
+
app.build_exchange(name: exchange, type: exchange_type)
|
|
252
|
+
app.build_queue(name: queue_name, opts: queue_opts)
|
|
253
|
+
app.queue.bind(app.exchange, routing_key: routing_key)
|
|
254
|
+
app.consume!
|
|
255
|
+
health_check_thread = start_health_check(app, queue_name: queue_name, exchange_name: exchange, exchange_type: exchange_type)
|
|
256
|
+
health_check_thread.wait_for_termination
|
|
257
|
+
raise 'Health check error: Forcing reconnect.'
|
|
258
|
+
rescue StandardError => e
|
|
259
|
+
# Esto lo pongo por que si levanto el rabbit y el consumer a la vez
|
|
260
|
+
# El rabbit esta una banda de tiempo hasta aceptar conexiones, por lo que
|
|
261
|
+
# el consumer explota 2 millones de veces, por lo tanto con esto hago
|
|
262
|
+
# la espera ocupada y me evito de ponerlo en el entrypoint-docker
|
|
263
|
+
Rails.logger.error("[RABBIT] Consumer error: #{e.message} (#{e.class})")
|
|
264
|
+
Rails.logger.debug("[RABBIT] Consumer error: #{e.backtrace}")
|
|
265
|
+
connection&.close
|
|
266
|
+
sleep RABBIT_NETWORK_RECOVERY
|
|
267
|
+
retry
|
|
268
|
+
end
|
|
96
269
|
|
|
97
|
-
|
|
98
|
-
|
|
270
|
+
def self.start_health_check(app, queue_name:, exchange_name:, exchange_type:)
|
|
271
|
+
task = Concurrent::TimerTask.new(execution_interval: RABBIT_HEALT_CHECK) do
|
|
272
|
+
# con esto veo si el exachange o la cola no la borraron desde la vista de rabbit
|
|
273
|
+
app.channel.exchange_declare(exchange_name, exchange_type, passive: true)
|
|
274
|
+
app.channel.queue_declare(queue_name, passive: true)
|
|
275
|
+
rescue Bunny::NotFound
|
|
276
|
+
Rails.logger.error("Health check failed: Queue '#{queue_name}' no longer exists!")
|
|
277
|
+
app.connection.close
|
|
278
|
+
task.shutdown # Detenemos la tarea para que no se ejecute de nuevo
|
|
279
|
+
rescue StandardError => e
|
|
280
|
+
Rails.logger.error("Health check error: #{e.message}. Forcing reconnect.")
|
|
281
|
+
app.connection.close
|
|
282
|
+
task.shutdown
|
|
283
|
+
end
|
|
99
284
|
|
|
100
|
-
|
|
101
|
-
|
|
285
|
+
task.execute
|
|
286
|
+
task
|
|
287
|
+
end
|
|
102
288
|
|
|
103
|
-
|
|
289
|
+
def self.create_connection(host: nil, username: nil, password: nil, vhost: nil)
|
|
290
|
+
bunny = Bunny.new(
|
|
291
|
+
host: host || BugBunny.configuration.host,
|
|
292
|
+
username: username || BugBunny.configuration.username,
|
|
293
|
+
password: password || BugBunny.configuration.password,
|
|
294
|
+
vhost: vhost || BugBunny.configuration.vhost,
|
|
295
|
+
logger: BugBunny.configuration.logger || Rails.logger,
|
|
296
|
+
automatically_recover: BugBunny.configuration.automatically_recover || false,
|
|
297
|
+
network_recovery_interval: BugBunny.configuration.network_recovery_interval || 5,
|
|
298
|
+
connection_timeout: BugBunny.configuration.connection_timeout || 10,
|
|
299
|
+
read_timeout: BugBunny.configuration.read_timeout || 90,
|
|
300
|
+
write_timeout: BugBunny.configuration.write_timeout || 90,
|
|
301
|
+
heartbeat: BugBunny.configuration.heartbeat || 30,
|
|
302
|
+
continuation_timeout: BugBunny.configuration.continuation_timeout || 15_000
|
|
303
|
+
)
|
|
104
304
|
|
|
105
|
-
|
|
305
|
+
bunny.tap(&:start)
|
|
106
306
|
end
|
|
107
307
|
end
|
|
108
308
|
end
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
module BugBunny
|
|
2
|
+
class Resource
|
|
3
|
+
include ActiveModel::API
|
|
4
|
+
include ActiveModel::Attributes
|
|
5
|
+
include ActiveModel::Dirty
|
|
6
|
+
include ActiveModel::Validations
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
attr_accessor :resource_path
|
|
10
|
+
attr_writer :resource_name
|
|
11
|
+
|
|
12
|
+
def resource_name
|
|
13
|
+
@resource_name ||= name.demodulize.underscore
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def inherited(subclass)
|
|
17
|
+
super
|
|
18
|
+
|
|
19
|
+
subclass.resource_path = resource_path if resource_path.present?
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class ExchangeScope
|
|
24
|
+
attr_reader :exchange_name, :klass
|
|
25
|
+
|
|
26
|
+
def initialize(klass, exchange_name)
|
|
27
|
+
@klass = klass
|
|
28
|
+
@exchange_name = exchange_name
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def method_missing(method_name, *args, **kwargs, &block)
|
|
32
|
+
if @klass.respond_to?(method_name, true)
|
|
33
|
+
kwargs[:exchange] = @exchange_name
|
|
34
|
+
@klass.execute(method_name.to_sym, *args, **kwargs, &block)
|
|
35
|
+
else
|
|
36
|
+
super
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
41
|
+
@klass.respond_to?(method_name, true) || super
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
attribute :persisted, :boolean, default: false
|
|
46
|
+
|
|
47
|
+
def initialize(attributes = {})
|
|
48
|
+
attributes.each do |key, value|
|
|
49
|
+
attribute_name = key.to_sym
|
|
50
|
+
type = guess_type(value)
|
|
51
|
+
|
|
52
|
+
next if self.class.attribute_names.include?(attribute_name.to_s)
|
|
53
|
+
|
|
54
|
+
self.class.attribute attribute_name, type if type.present?
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
super(attributes)
|
|
58
|
+
|
|
59
|
+
@previously_persisted = persisted
|
|
60
|
+
clear_changes_information if persisted?
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def assign_attributes(attrs)
|
|
64
|
+
return if attrs.blank?
|
|
65
|
+
|
|
66
|
+
attrs.each do |key, val|
|
|
67
|
+
setter_method = "#{key.to_s.underscore}="
|
|
68
|
+
|
|
69
|
+
next unless respond_to?(setter_method)
|
|
70
|
+
|
|
71
|
+
send(setter_method, val)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def guess_type(value)
|
|
76
|
+
case value
|
|
77
|
+
when Integer then :integer
|
|
78
|
+
when Float then :float
|
|
79
|
+
when Date then :date
|
|
80
|
+
when Time, DateTime then :datetime
|
|
81
|
+
when TrueClass, FalseClass then :boolean
|
|
82
|
+
when String then :string
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def persisted?
|
|
87
|
+
persisted
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def update(attrs_changes)
|
|
91
|
+
assign_attributes(attrs_changes)
|
|
92
|
+
save
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def changes_to_send
|
|
96
|
+
attrs = {}
|
|
97
|
+
changes.each { |attribute, values| attrs[attribute] = values[1] }
|
|
98
|
+
attrs
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def save
|
|
102
|
+
action = persisted? ? self.class.update_action.to_sym : self.class.create_action.to_sym
|
|
103
|
+
|
|
104
|
+
return self if persisted? && changes.empty?
|
|
105
|
+
|
|
106
|
+
obj = self.class.publisher.send(action, exchange: current_exchange, message: changes_to_send)
|
|
107
|
+
|
|
108
|
+
assign_attributes(obj) # refresco el objeto
|
|
109
|
+
self.persisted = true
|
|
110
|
+
@previously_persisted = true
|
|
111
|
+
clear_changes_information
|
|
112
|
+
true
|
|
113
|
+
rescue BugBunny::ResponseError::UnprocessableEntity => e
|
|
114
|
+
load_remote_rabbit_errors(e.message)
|
|
115
|
+
false
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def destroy
|
|
119
|
+
return self unless persisted?
|
|
120
|
+
|
|
121
|
+
# Llamada al PUBLISHER sin el argumento 'box'
|
|
122
|
+
self.class.publisher.send(destroy_action.to_sym, exchange: current_exchange, id: id)
|
|
123
|
+
|
|
124
|
+
self.persisted = false
|
|
125
|
+
true
|
|
126
|
+
rescue BugBunny::ResponseError::UnprocessableEntity => e
|
|
127
|
+
load_remote_rabbit_errors(e.message)
|
|
128
|
+
false
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def current_exchange
|
|
132
|
+
self.class.current_exchange
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def self.for_exchange(exchange_name)
|
|
136
|
+
raise ArgumentError, 'Exchange name must be specified.' if exchange_name.blank?
|
|
137
|
+
|
|
138
|
+
ExchangeScope.new(self, exchange_name)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def self.execute(name, *args, **kwargs, &block)
|
|
142
|
+
original_exchange = Thread.current[:bugbunny_current_exchange]
|
|
143
|
+
Thread.current[:bugbunny_current_exchange] = kwargs[:exchange]
|
|
144
|
+
begin
|
|
145
|
+
kwargs.delete(:exchange)
|
|
146
|
+
send(name, *args, **kwargs, &block)
|
|
147
|
+
ensure
|
|
148
|
+
Thread.current[:bugbunny_current_exchange] = original_exchange
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def self.current_exchange
|
|
153
|
+
Thread.current[:bugbunny_current_exchange]
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def self.all
|
|
157
|
+
where
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def self.where(query = {})
|
|
161
|
+
body = publisher.send(index_action.to_sym, exchange: current_exchange, message: query)
|
|
162
|
+
instances = []
|
|
163
|
+
|
|
164
|
+
body.each do |obj|
|
|
165
|
+
instance = new
|
|
166
|
+
instance.assign_attributes(obj.merge(persisted: true))
|
|
167
|
+
instances << instance
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
instances
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def self.find(id)
|
|
174
|
+
obj = publisher.send(show_action.to_sym, exchange: current_exchange, id: id)
|
|
175
|
+
return if obj.blank?
|
|
176
|
+
|
|
177
|
+
instance = new
|
|
178
|
+
instance.assign_attributes(obj.merge(persisted: true))
|
|
179
|
+
instance
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def self.create(payload)
|
|
183
|
+
instance = new(payload)
|
|
184
|
+
instance.save
|
|
185
|
+
instance
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def self.index_action
|
|
189
|
+
:index
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def self.show_action
|
|
193
|
+
:show
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def self.create_action
|
|
197
|
+
:create
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def self.update_action
|
|
201
|
+
:update
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def self.destroy_action
|
|
205
|
+
:destroy
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def self.publisher
|
|
209
|
+
@publisher ||= if resource_path.end_with?('/')
|
|
210
|
+
[resource_path, resource_name].join('').camelize.constantize
|
|
211
|
+
else
|
|
212
|
+
[resource_path, resource_name].join('/').camelize.constantize
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def load_remote_rabbit_errors(remote_errors)
|
|
217
|
+
JSON.parse(remote_errors).each do |attribute, errors|
|
|
218
|
+
errors.each do |error|
|
|
219
|
+
self.errors.add(attribute, error['error'], **error.except('error').symbolize_keys)
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
private_class_method :all, :where, :find, :create
|
|
225
|
+
end
|
|
226
|
+
end
|
data/lib/bug_bunny/version.rb
CHANGED
data/lib/bug_bunny.rb
CHANGED
|
@@ -1,22 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'bunny'
|
|
4
|
-
require_relative
|
|
5
|
-
require_relative
|
|
6
|
-
require_relative
|
|
7
|
-
require_relative
|
|
8
|
-
require_relative
|
|
9
|
-
require_relative
|
|
10
|
-
require_relative
|
|
11
|
-
require_relative "bug_bunny/rabbit"
|
|
12
|
-
require_relative "bug_bunny/response"
|
|
13
|
-
require_relative "bug_bunny/security"
|
|
14
|
-
require_relative "bug_bunny/helpers"
|
|
15
|
-
|
|
16
|
-
if defined? ::Rails::Railtie
|
|
17
|
-
## Rails only files
|
|
18
|
-
require 'bug_bunny/railtie'
|
|
19
|
-
end
|
|
4
|
+
require_relative 'bug_bunny/version'
|
|
5
|
+
require_relative 'bug_bunny/config'
|
|
6
|
+
require_relative 'bug_bunny/controller'
|
|
7
|
+
require_relative 'bug_bunny/publisher'
|
|
8
|
+
require_relative 'bug_bunny/exception'
|
|
9
|
+
require_relative 'bug_bunny/rabbit'
|
|
10
|
+
require_relative 'bug_bunny/resource'
|
|
20
11
|
|
|
21
12
|
module BugBunny
|
|
22
13
|
class << self
|