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.
@@ -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
- # Esta clase escucha en una cola RabbitMQ, parsea el header `type` como si fuera una URL,
12
- # extrae el controlador, acción, ID y query params, y despacha la ejecución al controlador correspondiente.
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
- # Parseo robusto de la URL (Path + Query Params)
66
- route_info = parse_route(properties.type)
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], # ID extraído del path (ej: /users/show/12)
73
- query_params: route_info[:params], # Hash de query params (ej: ?active=true)
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 (o namespace configurable)
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 for route '#{properties.type}': #{e.message}")
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
@@ -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 todos los
8
- # metadatos estándar del protocolo AMQP 0.9.1.
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
- # === ENRUTAMIENTO ===
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
- # === METADATOS AMQP ===
38
-
39
- # @return [String] Identificador de la aplicación origen (App ID).
40
- attr_accessor :app_id
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
- # Establece valores por defecto sensatos:
72
- # * Content-Type: application/json
73
- # * Timestamp: Ahora
74
- # * Persistent: false (Modo rápido/volátil por defecto)
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 la `action` actúa como tal.
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 || action
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 || action
60
+ type || path
105
61
  end
106
62
 
107
63
  # Genera el Hash de opciones limpio para la gema Bunny.
108
64
  #
109
- # Elimina las claves con valor `nil` (`compact`) para reducir el tamaño del paquete de red.
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: headers,
82
+ headers: final_headers,
123
83
  reply_to: reply_to,
124
84
  correlation_id: correlation_id
125
85
  }.compact