bug_bunny 3.0.1 → 3.0.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.
@@ -9,15 +9,21 @@ module BugBunny
9
9
  # Esta clase transforma operaciones CRUD estándar en peticiones RPC utilizando
10
10
  # verbos HTTP semánticos (GET, POST, PUT, DELETE) transportados sobre headers AMQP.
11
11
  #
12
- # @example
12
+ # También gestiona la serialización automática de parámetros ("wrapping") para
13
+ # compatibilidad con Strong Parameters de Rails.
14
+ #
15
+ # @example Definición de un recurso
13
16
  # class User < BugBunny::Resource
14
17
  # self.exchange = 'app.topic'
15
18
  # self.resource_name = 'users'
19
+ # # Opcional: Personalizar la clave raíz del JSON
20
+ # self.param_key = 'user_data'
16
21
  # end
17
22
  #
18
- # User.create(name: 'Gaby') # Envía POST 'users'
19
- # u = User.find(1) # Envía GET 'users/1'
20
- # u.destroy # Envía DELETE 'users/1'
23
+ # @example Uso con contexto temporal
24
+ # # La instancia 'user' recordará que debe usar la routing_key 'urgent'
25
+ # user = User.with(routing_key: 'urgent').new(name: 'Gaby')
26
+ # user.save # Enviará a la cola 'urgent' aunque estemos fuera del bloque .with
21
27
  class Resource
22
28
  include ActiveModel::API
23
29
  include ActiveModel::Dirty
@@ -26,17 +32,39 @@ module BugBunny
26
32
 
27
33
  define_model_callbacks :save, :create, :update, :destroy
28
34
 
35
+ # @return [HashWithIndifferentAccess] Contenedor de los atributos remotos (JSON crudo).
29
36
  attr_reader :remote_attributes
37
+
38
+ # @return [Boolean] Indica si el objeto ha sido guardado en el servicio remoto.
30
39
  attr_accessor :persisted
31
40
 
41
+ # @return [String, nil] Routing Key capturada en el momento de la instanciación.
42
+ attr_accessor :routing_key
43
+
44
+ # @return [String, nil] Exchange capturado en el momento de la instanciación.
45
+ attr_accessor :exchange
46
+
47
+ # @return [String, nil] Tipo de Exchange capturado en el momento de la instanciación.
48
+ attr_accessor :exchange_type
49
+
32
50
  class << self
33
- attr_writer :connection_pool, :exchange, :exchange_type, :resource_name, :routing_key
51
+ # Configuración heredable
52
+ attr_writer :connection_pool, :exchange, :exchange_type, :resource_name, :routing_key, :param_key
34
53
 
35
- # Resuelve la configuración buscando en la jerarquía de clases.
54
+ # Lee la configuración del Thread actual (usado por el scope .with).
55
+ # @api private
56
+ def thread_config(key)
57
+ Thread.current["bb_#{object_id}_#{key}"]
58
+ end
59
+
60
+ # Resuelve la configuración buscando en: 1. Thread (Scope), 2. Clase, 3. Herencia.
36
61
  # @api private
37
62
  def resolve_config(key, instance_var)
38
- thread_key = "bb_#{object_id}_#{key}"
39
- return Thread.current[thread_key] if Thread.current.key?(thread_key)
63
+ # 1. Prioridad: Contexto de hilo (.with)
64
+ val = thread_config(key)
65
+ return val if val
66
+
67
+ # 2. Prioridad: Jerarquía de clases
40
68
  target = self
41
69
  while target <= BugBunny::Resource
42
70
  value = target.instance_variable_get(instance_var)
@@ -46,15 +74,32 @@ module BugBunny
46
74
  nil
47
75
  end
48
76
 
77
+ # @return [ConnectionPool] El pool de conexiones asignado.
49
78
  def connection_pool; resolve_config(:pool, :@connection_pool); end
79
+
80
+ # @return [String] El exchange configurado.
81
+ # @raise [ArgumentError] Si no se ha definido un exchange.
50
82
  def current_exchange; resolve_config(:exchange, :@exchange) || raise(ArgumentError, "Exchange not defined"); end
83
+
84
+ # @return [String] El tipo de exchange (default: direct).
51
85
  def current_exchange_type; resolve_config(:exchange_type, :@exchange_type) || 'direct'; end
52
-
86
+
87
+ # @return [String] El nombre del recurso (ej: 'users'). Se infiere del nombre de la clase si no existe.
53
88
  def resource_name
54
89
  resolve_config(:resource_name, :@resource_name) || name.demodulize.underscore.pluralize
55
90
  end
56
91
 
57
- # Define middlewares para el cliente de este recurso.
92
+ # Define la clave raíz para envolver el payload JSON (Wrapping).
93
+ #
94
+ # Por defecto utiliza `model_name.element`, lo que elimina los namespaces.
95
+ # Ej: `Manager::Service` -> `'service'`.
96
+ #
97
+ # @return [String] La clave paramétrica.
98
+ def param_key
99
+ resolve_config(:param_key, :@param_key) || model_name.element
100
+ end
101
+
102
+ # Define un middleware para el cliente HTTP/AMQP de este recurso.
58
103
  def client_middleware(&block)
59
104
  @client_middleware_stack ||= []
60
105
  @client_middleware_stack << block
@@ -72,6 +117,8 @@ module BugBunny
72
117
  stack
73
118
  end
74
119
 
120
+ # Instancia un cliente configurado con el pool y middlewares del recurso.
121
+ # @return [BugBunny::Client]
75
122
  def bug_bunny_client
76
123
  pool = connection_pool
77
124
  raise BugBunny::Error, "Connection pool missing for #{name}" unless pool
@@ -81,14 +128,22 @@ module BugBunny
81
128
  end
82
129
  end
83
130
 
131
+ # Ejecuta un bloque (o retorna un Proxy) con una configuración temporal.
132
+ # Útil para cambiar de exchange o routing_key para una operación específica.
133
+ #
134
+ # @example
135
+ # User.with(routing_key: 'urgent').create(params)
84
136
  def with(exchange: nil, routing_key: nil, exchange_type: nil, pool: nil)
85
137
  keys = { exchange: "bb_#{object_id}_exchange", exchange_type: "bb_#{object_id}_exchange_type", pool: "bb_#{object_id}_pool", routing_key: "bb_#{object_id}_routing_key" }
86
138
  old_values = {}
87
139
  keys.each { |k, v| old_values[k] = Thread.current[v] }
140
+
141
+ # Seteamos valores temporales
88
142
  Thread.current[keys[:exchange]] = exchange if exchange
89
143
  Thread.current[keys[:exchange_type]] = exchange_type if exchange_type
90
144
  Thread.current[keys[:pool]] = pool if pool
91
145
  Thread.current[keys[:routing_key]] = routing_key if routing_key
146
+
92
147
  if block_given?
93
148
  begin; yield; ensure; keys.each { |k, v| Thread.current[v] = old_values[k] }; end
94
149
  else
@@ -96,36 +151,41 @@ module BugBunny
96
151
  end
97
152
  end
98
153
 
154
+ # Proxy para permitir encadenamiento: User.with(...).find(1)
99
155
  class ScopeProxy < BasicObject
100
156
  def initialize(target, keys, old_values); @target = target; @keys = keys; @old_values = old_values; end
101
157
  def method_missing(method, *args, &block); @target.public_send(method, *args, &block); ensure; @keys.each { |k, v| ::Thread.current[v] = @old_values[k] }; end
102
158
  end
103
159
 
104
160
  # Calcula la Routing Key.
105
- # @note En REST, por defecto es el resource_name (Topic 'users'), pero puede ser forzada.
161
+ # @return [String]
106
162
  def calculate_routing_key(id = nil)
107
- manual_rk = Thread.current["bb_#{object_id}_routing_key"]
163
+ # 1. Contexto .with
164
+ manual_rk = thread_config(:routing_key)
108
165
  return manual_rk if manual_rk
166
+
167
+ # 2. Configuración estática
109
168
  static_rk = resolve_config(:routing_key, :@routing_key)
110
169
  return static_rk if static_rk.present?
170
+
171
+ # 3. Default: Resource name
111
172
  resource_name
112
173
  end
113
174
 
114
- # @!group Acciones CRUD RESTful
175
+ # @!group Acciones CRUD RESTful (Clase)
115
176
 
116
- # GET resource?query
117
- #
118
- # @param filters [Hash] Parámetros de consulta (Query params).
177
+ # Busca recursos que coincidan con los filtros.
178
+ # Envía: GET resource?query
119
179
  def where(filters = {})
120
180
  rk = calculate_routing_key
121
181
  path = resource_name
122
182
  path += "?#{URI.encode_www_form(filters)}" if filters.present?
123
183
 
124
- # REST: GET collection
125
184
  response = bug_bunny_client.request(path, method: :get, exchange: current_exchange, exchange_type: current_exchange_type, routing_key: rk)
126
185
 
127
186
  return [] unless response['body'].is_a?(Array)
128
187
  response['body'].map do |attrs|
188
+ # Al instanciar aquí, se captura el contexto si estamos dentro de un .with
129
189
  inst = new(attrs)
130
190
  inst.persisted = true
131
191
  inst.send(:clear_changes_information)
@@ -135,14 +195,12 @@ module BugBunny
135
195
 
136
196
  def all; where({}); end
137
197
 
138
- # GET resource/id
139
- #
140
- # @param id [String, Integer] ID del recurso.
198
+ # Busca un recurso por ID.
199
+ # Envía: GET resource/id
141
200
  def find(id)
142
201
  rk = calculate_routing_key(id)
143
202
  path = "#{resource_name}/#{id}"
144
203
 
145
- # REST: GET member
146
204
  response = bug_bunny_client.request(path, method: :get, exchange: current_exchange, exchange_type: current_exchange_type, routing_key: rk)
147
205
 
148
206
  return nil if response.nil? || response['status'] == 404
@@ -156,6 +214,7 @@ module BugBunny
156
214
  instance
157
215
  end
158
216
 
217
+ # Crea un nuevo recurso.
159
218
  def create(payload)
160
219
  instance = new(payload)
161
220
  instance.save
@@ -165,18 +224,45 @@ module BugBunny
165
224
 
166
225
  # @!group Instancia
167
226
 
168
- def current_exchange; self.class.current_exchange; end
169
- def current_exchange_type; self.class.current_exchange_type; end
170
- def calculate_routing_key(id=nil); self.class.calculate_routing_key(id); end
171
- def bug_bunny_client; self.class.bug_bunny_client; end
172
-
227
+ # Inicializa una nueva instancia del recurso.
228
+ #
229
+ # **IMPORTANTE:** Captura la configuración del contexto actual (`.with`)
230
+ # y la guarda en la instancia. Esto permite que objetos creados dentro de un bloque `with`
231
+ # mantengan esa configuración (routing_key, exchange) durante todo su ciclo de vida,
232
+ # incluso si `save` se llama fuera del bloque.
233
+ #
234
+ # @param attributes [Hash] Atributos iniciales.
173
235
  def initialize(attributes = {})
174
236
  @remote_attributes = {}.with_indifferent_access
175
237
  @persisted = false
238
+
239
+ # === CAPTURA DE CONTEXTO ===
240
+ @routing_key = self.class.thread_config(:routing_key)
241
+ @exchange = self.class.thread_config(:exchange)
242
+ @exchange_type = self.class.thread_config(:exchange_type)
243
+
176
244
  assign_attributes(attributes)
177
245
  super()
178
246
  end
179
247
 
248
+ # Prioridad Routing Key: 1. Instancia (Capturada), 2. Clase
249
+ def calculate_routing_key(id=nil)
250
+ return @routing_key if @routing_key
251
+ self.class.calculate_routing_key(id)
252
+ end
253
+
254
+ # Prioridad Exchange: 1. Instancia (Capturada), 2. Clase
255
+ def current_exchange
256
+ @exchange || self.class.current_exchange
257
+ end
258
+
259
+ # Prioridad Exchange Type: 1. Instancia (Capturada), 2. Clase
260
+ def current_exchange_type
261
+ @exchange_type || self.class.current_exchange_type
262
+ end
263
+
264
+ def bug_bunny_client; self.class.bug_bunny_client; end
265
+
180
266
  def persisted?; !!@persisted; end
181
267
 
182
268
  def assign_attributes(new_attributes)
@@ -189,12 +275,13 @@ module BugBunny
189
275
  save
190
276
  end
191
277
 
278
+ # Retorna solo los atributos que han cambiado.
192
279
  def changes_to_send
193
280
  return changes.transform_values(&:last) unless changes.empty?
194
281
  @remote_attributes.except('id', 'ID', 'Id', '_id')
195
282
  end
196
283
 
197
- # Magic Methods para atributos dinámicos
284
+ # Métodos mágicos para atributos.
198
285
  def method_missing(method_name, *args, &block)
199
286
  attribute_name = method_name.to_s
200
287
  if attribute_name.end_with?('=')
@@ -225,7 +312,13 @@ module BugBunny
225
312
 
226
313
  # @!group Persistencia RESTful
227
314
 
228
- # Guarda el registro (POST si es nuevo, PUT si existe).
315
+ # Guarda el registro.
316
+ # Envía POST si es nuevo, PUT si ya existe.
317
+ #
318
+ # **AUTOMÁTICO:** Envuelve los parámetros en la clave del modelo (`param_key`).
319
+ # Ej: Manager::Service -> "service". Esto facilita `params.require(:service)`.
320
+ #
321
+ # @return [Boolean] true si se guardó correctamente.
229
322
  def save
230
323
  return false unless valid?
231
324
 
@@ -233,15 +326,22 @@ module BugBunny
233
326
  is_new = !persisted?
234
327
  rk = calculate_routing_key(id)
235
328
 
236
- # Mapeo a verbos HTTP usando client.request(method: ...)
329
+ # 1. Obtenemos el payload plano (atributos modificados)
330
+ flat_payload = changes_to_send
331
+
332
+ # 2. Wrappeamos automáticamente en la clave del modelo
333
+ key = self.class.param_key
334
+ wrapped_payload = { key => flat_payload }
335
+
336
+ # Mapeo a verbos HTTP
237
337
  if is_new
238
338
  # REST: POST resource (Create)
239
339
  path = self.class.resource_name
240
- response = bug_bunny_client.request(path, method: :post, exchange: current_exchange, exchange_type: current_exchange_type, routing_key: rk, body: changes_to_send)
340
+ response = bug_bunny_client.request(path, method: :post, exchange: current_exchange, exchange_type: current_exchange_type, routing_key: rk, body: wrapped_payload)
241
341
  else
242
342
  # REST: PUT resource/id (Update)
243
343
  path = "#{self.class.resource_name}/#{id}"
244
- response = bug_bunny_client.request(path, method: :put, exchange: current_exchange, exchange_type: current_exchange_type, routing_key: rk, body: changes_to_send)
344
+ response = bug_bunny_client.request(path, method: :put, exchange: current_exchange, exchange_type: current_exchange_type, routing_key: rk, body: wrapped_payload)
245
345
  end
246
346
 
247
347
  handle_save_response(response)
@@ -251,12 +351,12 @@ module BugBunny
251
351
  false
252
352
  end
253
353
 
254
- # Elimina el registro (DELETE).
354
+ # Elimina el registro.
355
+ # Envía DELETE resource/id.
255
356
  def destroy
256
357
  return false unless persisted?
257
358
 
258
359
  run_callbacks(:destroy) do
259
- # REST: DELETE resource/id
260
360
  path = "#{self.class.resource_name}/#{id}"
261
361
  rk = calculate_routing_key(id)
262
362
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BugBunny
4
- VERSION = "3.0.1"
4
+ VERSION = "3.0.2"
5
5
  end
data/lib/bug_bunny.rb CHANGED
@@ -95,7 +95,7 @@ module BugBunny
95
95
  username: options[:username] || default.username,
96
96
  password: options[:password] || default.password,
97
97
  vhost: options[:vhost] || default.vhost,
98
- logger: options[:logger] || default.logger,
98
+ logger: options[:logger] || default.bunny_logger,
99
99
  automatically_recover: options[:automatically_recover] || default.automatically_recover,
100
100
  network_recovery_interval: options[:network_recovery_interval] || default.network_recovery_interval,
101
101
  connection_timeout: options[:connection_timeout] || default.connection_timeout,
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bug_bunny
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.1
4
+ version: 3.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - gabix
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-02-10 00:00:00.000000000 Z
11
+ date: 2026-02-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bunny
@@ -100,14 +100,14 @@ dependencies:
100
100
  requirements:
101
101
  - - ">="
102
102
  - !ruby/object:Gem::Version
103
- version: '0'
103
+ version: '2.0'
104
104
  type: :runtime
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - ">="
109
109
  - !ruby/object:Gem::Version
110
- version: '0'
110
+ version: '2.0'
111
111
  - !ruby/object:Gem::Dependency
112
112
  name: ostruct
113
113
  requirement: !ruby/object:Gem::Requirement
@@ -128,57 +128,59 @@ dependencies:
128
128
  requirements:
129
129
  - - ">="
130
130
  - !ruby/object:Gem::Version
131
- version: '0'
131
+ version: '2.0'
132
132
  type: :development
133
133
  prerelease: false
134
134
  version_requirements: !ruby/object:Gem::Requirement
135
135
  requirements:
136
136
  - - ">="
137
137
  - !ruby/object:Gem::Version
138
- version: '0'
138
+ version: '2.0'
139
139
  - !ruby/object:Gem::Dependency
140
140
  name: rake
141
141
  requirement: !ruby/object:Gem::Requirement
142
142
  requirements:
143
- - - ">="
143
+ - - "~>"
144
144
  - !ruby/object:Gem::Version
145
- version: '0'
145
+ version: '13.0'
146
146
  type: :development
147
147
  prerelease: false
148
148
  version_requirements: !ruby/object:Gem::Requirement
149
149
  requirements:
150
- - - ">="
150
+ - - "~>"
151
151
  - !ruby/object:Gem::Version
152
- version: '0'
152
+ version: '13.0'
153
153
  - !ruby/object:Gem::Dependency
154
154
  name: rspec
155
155
  requirement: !ruby/object:Gem::Requirement
156
156
  requirements:
157
- - - ">="
157
+ - - "~>"
158
158
  - !ruby/object:Gem::Version
159
- version: '0'
159
+ version: '3.0'
160
160
  type: :development
161
161
  prerelease: false
162
162
  version_requirements: !ruby/object:Gem::Requirement
163
163
  requirements:
164
- - - ">="
164
+ - - "~>"
165
165
  - !ruby/object:Gem::Version
166
- version: '0'
166
+ version: '3.0'
167
167
  - !ruby/object:Gem::Dependency
168
168
  name: yard
169
169
  requirement: !ruby/object:Gem::Requirement
170
170
  requirements:
171
- - - ">="
171
+ - - "~>"
172
172
  - !ruby/object:Gem::Version
173
- version: '0'
173
+ version: '0.9'
174
174
  type: :development
175
175
  prerelease: false
176
176
  version_requirements: !ruby/object:Gem::Requirement
177
177
  requirements:
178
- - - ">="
178
+ - - "~>"
179
179
  - !ruby/object:Gem::Version
180
- version: '0'
181
- description: Gem for sync and async comunication via rabbit bunny.
180
+ version: '0.9'
181
+ description: BugBunny is a lightweight RPC framework for Ruby on Rails over RabbitMQ.
182
+ It simulates a RESTful architecture with an intelligent router, Active Record-like
183
+ resources, and middleware support.
182
184
  email:
183
185
  - gab.edera@gmail.com
184
186
  executables: []