bug_bunny 3.0.0 → 3.0.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 +15 -0
- data/README.md +142 -100
- data/lib/bug_bunny/client.rb +22 -39
- data/lib/bug_bunny/consumer.rb +65 -55
- data/lib/bug_bunny/request.rb +30 -70
- data/lib/bug_bunny/resource.rb +82 -229
- data/lib/bug_bunny/version.rb +1 -1
- metadata +2 -2
data/lib/bug_bunny/consumer.rb
CHANGED
|
@@ -6,15 +6,13 @@ require 'uri'
|
|
|
6
6
|
require 'cgi'
|
|
7
7
|
|
|
8
8
|
module BugBunny
|
|
9
|
-
# Consumidor de mensajes y Router RPC.
|
|
9
|
+
# Consumidor de mensajes y Router RPC estilo REST.
|
|
10
10
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
11
|
+
# Parsea el header `type` (URL) y el header `x-http-method` (Verbo)
|
|
12
|
+
# para despachar al controlador y acción correctos siguiendo convenciones Rails.
|
|
13
13
|
class Consumer
|
|
14
14
|
attr_reader :session
|
|
15
15
|
|
|
16
|
-
# Método factory para instanciar y suscribir.
|
|
17
|
-
# @see #subscribe
|
|
18
16
|
def self.subscribe(connection:, **args)
|
|
19
17
|
new(connection).subscribe(**args)
|
|
20
18
|
end
|
|
@@ -24,12 +22,6 @@ module BugBunny
|
|
|
24
22
|
end
|
|
25
23
|
|
|
26
24
|
# Inicia la suscripción a la cola.
|
|
27
|
-
#
|
|
28
|
-
# @param queue_name [String] Cola a escuchar.
|
|
29
|
-
# @param exchange_name [String] Exchange para binding.
|
|
30
|
-
# @param routing_key [String] Routing key.
|
|
31
|
-
# @param exchange_type [String] Tipo de exchange.
|
|
32
|
-
# @param block [Boolean] Bloquear el hilo principal (loop).
|
|
33
25
|
def subscribe(queue_name:, exchange_name:, routing_key:, exchange_type: 'direct', queue_opts: {}, block: true)
|
|
34
26
|
x = session.exchange(name: exchange_name, type: exchange_type)
|
|
35
27
|
q = session.queue(queue_name, queue_opts)
|
|
@@ -50,11 +42,7 @@ module BugBunny
|
|
|
50
42
|
private
|
|
51
43
|
|
|
52
44
|
# Procesa el mensaje entrante.
|
|
53
|
-
#
|
|
54
|
-
# 1. Parsea la "URL" del header `type`.
|
|
55
|
-
# 2. Instancia el controlador dinámicamente.
|
|
56
|
-
# 3. Ejecuta la acción.
|
|
57
|
-
# 4. Envía respuesta RPC si `reply_to` está presente.
|
|
45
|
+
# Infiere la acción basándose en Verbo + URL.
|
|
58
46
|
def process_message(delivery_info, properties, body)
|
|
59
47
|
if properties.type.nil? || properties.type.empty?
|
|
60
48
|
BugBunny.configuration.logger.error("[Consumer] Missing 'type'. Rejected.")
|
|
@@ -62,21 +50,26 @@ module BugBunny
|
|
|
62
50
|
return
|
|
63
51
|
end
|
|
64
52
|
|
|
65
|
-
#
|
|
66
|
-
|
|
53
|
+
# 1. Leemos el verbo HTTP desde el header (Default: GET)
|
|
54
|
+
# Nota: Bunny devuelve los headers en propiedades.headers
|
|
55
|
+
http_method = properties.headers ? (properties.headers['x-http-method'] || 'GET') : 'GET'
|
|
56
|
+
|
|
57
|
+
# 2. Despachamos usando lógica Rails
|
|
58
|
+
route_info = router_dispatch(http_method, properties.type)
|
|
67
59
|
|
|
68
60
|
headers = {
|
|
69
61
|
type: properties.type,
|
|
62
|
+
http_method: http_method,
|
|
70
63
|
controller: route_info[:controller],
|
|
71
64
|
action: route_info[:action],
|
|
72
|
-
id: route_info[:id],
|
|
73
|
-
query_params: route_info[:params],
|
|
65
|
+
id: route_info[:id],
|
|
66
|
+
query_params: route_info[:params],
|
|
74
67
|
content_type: properties.content_type,
|
|
75
68
|
correlation_id: properties.correlation_id,
|
|
76
69
|
reply_to: properties.reply_to
|
|
77
70
|
}
|
|
78
71
|
|
|
79
|
-
# Convention: "users" -> Rabbit::Controllers::UsersController
|
|
72
|
+
# Convention: "users" -> Rabbit::Controllers::UsersController
|
|
80
73
|
controller_class_name = "rabbit/controllers/#{route_info[:controller]}".camelize
|
|
81
74
|
controller_class = controller_class_name.constantize
|
|
82
75
|
|
|
@@ -88,7 +81,7 @@ module BugBunny
|
|
|
88
81
|
|
|
89
82
|
session.channel.ack(delivery_info.delivery_tag)
|
|
90
83
|
rescue NameError => e
|
|
91
|
-
BugBunny.configuration.logger.error("[Consumer] Controller not found
|
|
84
|
+
BugBunny.configuration.logger.error("[Consumer] Controller/Action not found: #{e.message}")
|
|
92
85
|
session.channel.reject(delivery_info.delivery_tag, false)
|
|
93
86
|
rescue StandardError => e
|
|
94
87
|
BugBunny.configuration.logger.error("[Consumer] Execution Error: #{e.message}")
|
|
@@ -98,6 +91,56 @@ module BugBunny
|
|
|
98
91
|
end
|
|
99
92
|
end
|
|
100
93
|
|
|
94
|
+
# Router: Simula el config/routes.rb de Rails.
|
|
95
|
+
#
|
|
96
|
+
# @param method [String] Verbo HTTP (GET, POST, etc).
|
|
97
|
+
# @param path [String] URL Path (ej: 'users/1').
|
|
98
|
+
# @return [Hash] {controller, action, id, params}
|
|
99
|
+
def router_dispatch(method, path)
|
|
100
|
+
uri = URI.parse("http://dummy/#{path}")
|
|
101
|
+
segments = uri.path.split('/').reject(&:empty?) # ["users", "123"]
|
|
102
|
+
query_params = uri.query ? CGI.parse(uri.query).transform_values(&:first) : {}
|
|
103
|
+
|
|
104
|
+
controller_name = segments[0] # "users"
|
|
105
|
+
id = segments[1] # "123" o nil
|
|
106
|
+
|
|
107
|
+
# Lógica de Inferencia Rails Standard
|
|
108
|
+
# GET users -> index
|
|
109
|
+
# GET users/1 -> show
|
|
110
|
+
# POST users -> create
|
|
111
|
+
# PUT users/1 -> update
|
|
112
|
+
# DELETE users/1 -> destroy
|
|
113
|
+
action = case method.to_s.upcase
|
|
114
|
+
when 'GET'
|
|
115
|
+
id ? 'show' : 'index'
|
|
116
|
+
when 'POST'
|
|
117
|
+
'create'
|
|
118
|
+
when 'PUT', 'PATCH'
|
|
119
|
+
'update'
|
|
120
|
+
when 'DELETE'
|
|
121
|
+
'destroy'
|
|
122
|
+
else
|
|
123
|
+
id || 'index' # Fallback para verbos custom
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Soporte para Member Actions Custom (ej: POST users/1/activate)
|
|
127
|
+
# Path: users/1/activate -> segments: [users, 1, activate]
|
|
128
|
+
if segments.size >= 3
|
|
129
|
+
id = segments[1]
|
|
130
|
+
action = segments[2]
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Inyectar ID en params para acceso unificado en el controller
|
|
134
|
+
query_params['id'] = id if id
|
|
135
|
+
|
|
136
|
+
{
|
|
137
|
+
controller: controller_name,
|
|
138
|
+
action: action,
|
|
139
|
+
id: id,
|
|
140
|
+
params: query_params
|
|
141
|
+
}
|
|
142
|
+
end
|
|
143
|
+
|
|
101
144
|
def reply(payload, reply_to, correlation_id)
|
|
102
145
|
session.channel.default_exchange.publish(
|
|
103
146
|
payload.to_json,
|
|
@@ -114,38 +157,5 @@ module BugBunny
|
|
|
114
157
|
session.close
|
|
115
158
|
end.execute
|
|
116
159
|
end
|
|
117
|
-
|
|
118
|
-
# Analiza el string `type` como una URL.
|
|
119
|
-
#
|
|
120
|
-
# Soporta:
|
|
121
|
-
# * `users/index?active=true` -> {controller: users, action: index, params: {active: true}}
|
|
122
|
-
# * `users/show/12` -> {controller: users, action: show, id: 12}
|
|
123
|
-
# * `users/update/12` -> {controller: users, action: update, id: 12}
|
|
124
|
-
#
|
|
125
|
-
# @param route_string [String] El valor del header type.
|
|
126
|
-
# @return [Hash] Keys: :controller, :action, :id, :params.
|
|
127
|
-
def parse_route(route_string)
|
|
128
|
-
# Anteponemos un host dummy para usar URI estándar con paths relativos
|
|
129
|
-
uri = URI.parse("http://dummy/#{route_string}")
|
|
130
|
-
# 1. Query Params (?foo=bar)
|
|
131
|
-
query_params = uri.query ? CGI.parse(uri.query).transform_values(&:first) : {}
|
|
132
|
-
|
|
133
|
-
# 2. Path Segments (/users/show/12)
|
|
134
|
-
segments = uri.path.split('/').reject(&:empty?)
|
|
135
|
-
|
|
136
|
-
controller = segments[0] # "users"
|
|
137
|
-
action = segments[1] # "index", "show", "update"
|
|
138
|
-
id = segments[2] # "12" (opcional)
|
|
139
|
-
|
|
140
|
-
# Inyectamos el ID en los params si existe en la ruta para facilitar acceso unificado
|
|
141
|
-
query_params['id'] = id if id
|
|
142
|
-
|
|
143
|
-
{
|
|
144
|
-
controller: controller,
|
|
145
|
-
action: action || 'index',
|
|
146
|
-
id: id,
|
|
147
|
-
params: query_params
|
|
148
|
-
}
|
|
149
|
-
end
|
|
150
160
|
end
|
|
151
161
|
end
|
data/lib/bug_bunny/request.rb
CHANGED
|
@@ -4,79 +4,37 @@ 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 y
|
|
8
|
-
#
|
|
7
|
+
# Contiene el cuerpo del mensaje, la configuración de enrutamiento y el **Verbo HTTP**.
|
|
8
|
+
#
|
|
9
|
+
# @attr body [Object] El cuerpo del mensaje (Hash, Array o String).
|
|
10
|
+
# @attr headers [Hash] Cabeceras personalizadas (Headers AMQP).
|
|
11
|
+
# @attr path [String] La ruta lógica del recurso (ej: 'users', 'users/123').
|
|
12
|
+
# @attr method [Symbol, String] El verbo HTTP (:get, :post, :put, :delete). Default: :get.
|
|
13
|
+
# @attr exchange [String] El nombre del Exchange destino.
|
|
14
|
+
# @attr exchange_type [String] El tipo de exchange ('direct', 'topic', 'fanout').
|
|
15
|
+
# @attr routing_key [String] La routing key específica. Si es nil, se usará {#path}.
|
|
16
|
+
# @attr timeout [Integer] Tiempo máximo en segundos para timeout RPC.
|
|
9
17
|
class Request
|
|
10
|
-
# === DATOS (Payload) ===
|
|
11
|
-
|
|
12
|
-
# @return [Object] El cuerpo del mensaje (Hash, Array o String) antes de ser serializado.
|
|
13
18
|
attr_accessor :body
|
|
14
|
-
|
|
15
|
-
# @return [Hash] Cabeceras personalizadas (Headers AMQP) para pasar metadatos extra.
|
|
16
19
|
attr_accessor :headers
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
# @return [String] La "ruta" lógica del mensaje (ej: 'users/create'). Se usa por defecto como routing key y type.
|
|
21
|
-
attr_accessor :action
|
|
22
|
-
|
|
23
|
-
# @return [String] El nombre del Exchange destino donde se publicará el mensaje.
|
|
20
|
+
attr_accessor :path
|
|
21
|
+
attr_accessor :method
|
|
24
22
|
attr_accessor :exchange
|
|
25
|
-
|
|
26
|
-
# @return [String] El tipo de exchange ('direct', 'topic', 'fanout', 'headers'). Default: 'direct'.
|
|
27
23
|
attr_accessor :exchange_type
|
|
28
|
-
|
|
29
|
-
# @return [String] La routing key específica para RabbitMQ. Si es nil, se usará {#action}.
|
|
30
24
|
attr_accessor :routing_key
|
|
31
|
-
|
|
32
|
-
# === CONFIGURACIÓN ===
|
|
33
|
-
|
|
34
|
-
# @return [Integer] Tiempo máximo en segundos que el cliente RPC esperará la respuesta.
|
|
35
25
|
attr_accessor :timeout
|
|
36
26
|
|
|
37
|
-
#
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
# @return [String] Tipo MIME del contenido (Default: 'application/json').
|
|
43
|
-
attr_accessor :content_type
|
|
44
|
-
|
|
45
|
-
# @return [String] Codificación del contenido (ej: 'gzip').
|
|
46
|
-
attr_accessor :content_encoding
|
|
47
|
-
|
|
48
|
-
# @return [Integer] Prioridad del mensaje (0-9).
|
|
49
|
-
attr_accessor :priority
|
|
50
|
-
|
|
51
|
-
# @return [Integer] Timestamp UNIX del momento de creación.
|
|
52
|
-
attr_accessor :timestamp
|
|
53
|
-
|
|
54
|
-
# @return [String, Integer] Tiempo de vida (TTL) del mensaje en milisegundos.
|
|
55
|
-
attr_accessor :expiration
|
|
56
|
-
|
|
57
|
-
# @return [Boolean] Si es `true`, el mensaje se guardará en disco (más lento, más seguro). Default: `false`.
|
|
58
|
-
attr_accessor :persistent
|
|
59
|
-
|
|
60
|
-
# @return [String] Cola específica donde se espera la respuesta (usado internamente para RPC).
|
|
61
|
-
attr_accessor :reply_to
|
|
62
|
-
|
|
63
|
-
# @return [String] ID único para correlacionar petición y respuesta (RPC).
|
|
64
|
-
attr_accessor :correlation_id
|
|
65
|
-
|
|
66
|
-
# @return [String] Sobrescribe el header 'type' de AMQP. Vital para el enrutamiento en el Consumer.
|
|
67
|
-
attr_accessor :type
|
|
27
|
+
# Metadatos AMQP Estándar
|
|
28
|
+
attr_accessor :app_id, :content_type, :content_encoding, :priority,
|
|
29
|
+
:timestamp, :expiration, :persistent, :reply_to,
|
|
30
|
+
:correlation_id, :type
|
|
68
31
|
|
|
69
32
|
# Inicializa un nuevo Request.
|
|
70
33
|
#
|
|
71
|
-
#
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
# * Exchange Type: direct
|
|
76
|
-
#
|
|
77
|
-
# @param action [String] La acción o ruta lógica del mensaje (ej: 'users/update').
|
|
78
|
-
def initialize(action)
|
|
79
|
-
@action = action
|
|
34
|
+
# @param path [String] La ruta del recurso o acción (ej: 'users/123').
|
|
35
|
+
def initialize(path)
|
|
36
|
+
@path = path
|
|
37
|
+
@method = :get # Verbo por defecto
|
|
80
38
|
@headers = {}
|
|
81
39
|
@content_type = 'application/json'
|
|
82
40
|
@timestamp = Time.now.to_i
|
|
@@ -87,29 +45,31 @@ module BugBunny
|
|
|
87
45
|
# Calcula la Routing Key final que se usará en RabbitMQ.
|
|
88
46
|
#
|
|
89
47
|
# Principio: "Convention over Configuration".
|
|
90
|
-
# Si no se define una `routing_key` manual, se asume que
|
|
48
|
+
# Si no se define una `routing_key` manual, se asume que el `path` actúa como tal.
|
|
91
49
|
#
|
|
92
50
|
# @return [String] La routing key definitiva.
|
|
93
51
|
def final_routing_key
|
|
94
|
-
routing_key ||
|
|
52
|
+
routing_key || path
|
|
95
53
|
end
|
|
96
54
|
|
|
97
55
|
# Calcula el valor para el header AMQP 'type'.
|
|
98
|
-
#
|
|
99
|
-
# Este valor es utilizado por {BugBunny::Consumer} para decidir qué Controlador ejecutar.
|
|
100
|
-
# Si no se define manualmente, usa la `action`.
|
|
56
|
+
# En esta arquitectura REST, el 'type' es la URL del recurso (el path).
|
|
101
57
|
#
|
|
102
58
|
# @return [String] El tipo de mensaje definitivo.
|
|
103
59
|
def final_type
|
|
104
|
-
type ||
|
|
60
|
+
type || path
|
|
105
61
|
end
|
|
106
62
|
|
|
107
63
|
# Genera el Hash de opciones limpio para la gema Bunny.
|
|
108
64
|
#
|
|
109
|
-
#
|
|
65
|
+
# **Importante:** Inyecta el verbo HTTP en los headers bajo la clave `x-http-method`.
|
|
66
|
+
# Esto permite al Consumer enrutar correctamente a la acción del controlador.
|
|
110
67
|
#
|
|
111
68
|
# @return [Hash] Opciones listas para pasar a `exchange.publish`.
|
|
112
69
|
def amqp_options
|
|
70
|
+
# Inyectamos el verbo HTTP en los headers para el Router del Consumer
|
|
71
|
+
final_headers = headers.merge('x-http-method' => method.to_s.upcase)
|
|
72
|
+
|
|
113
73
|
{
|
|
114
74
|
type: final_type,
|
|
115
75
|
app_id: app_id,
|
|
@@ -119,7 +79,7 @@ module BugBunny
|
|
|
119
79
|
timestamp: timestamp,
|
|
120
80
|
expiration: expiration,
|
|
121
81
|
persistent: persistent,
|
|
122
|
-
headers:
|
|
82
|
+
headers: final_headers,
|
|
123
83
|
reply_to: reply_to,
|
|
124
84
|
correlation_id: correlation_id
|
|
125
85
|
}.compact
|