bug_bunny 3.0.1 → 3.0.3

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.
@@ -2,6 +2,7 @@
2
2
  require 'active_model'
3
3
  require 'active_support/core_ext/string/inflections'
4
4
  require 'uri'
5
+ require 'rack/utils'
5
6
 
6
7
  module BugBunny
7
8
  # Clase base para modelos remotos que implementan **Active Record over AMQP (RESTful)**.
@@ -9,15 +10,21 @@ module BugBunny
9
10
  # Esta clase transforma operaciones CRUD estándar en peticiones RPC utilizando
10
11
  # verbos HTTP semánticos (GET, POST, PUT, DELETE) transportados sobre headers AMQP.
11
12
  #
12
- # @example
13
+ # También gestiona la serialización automática de parámetros ("wrapping") para
14
+ # compatibilidad con Strong Parameters de Rails.
15
+ #
16
+ # @example Definición de un recurso
13
17
  # class User < BugBunny::Resource
14
18
  # self.exchange = 'app.topic'
15
19
  # self.resource_name = 'users'
20
+ # # Opcional: Personalizar la clave raíz del JSON
21
+ # self.param_key = 'user_data'
16
22
  # end
17
23
  #
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'
24
+ # @example Uso con contexto temporal
25
+ # # La instancia 'user' recordará que debe usar la routing_key 'urgent'
26
+ # user = User.with(routing_key: 'urgent').new(name: 'Gaby')
27
+ # user.save # Enviará a la cola 'urgent' aunque estemos fuera del bloque .with
21
28
  class Resource
22
29
  include ActiveModel::API
23
30
  include ActiveModel::Dirty
@@ -26,17 +33,39 @@ module BugBunny
26
33
 
27
34
  define_model_callbacks :save, :create, :update, :destroy
28
35
 
36
+ # @return [HashWithIndifferentAccess] Contenedor de los atributos remotos (JSON crudo).
29
37
  attr_reader :remote_attributes
38
+
39
+ # @return [Boolean] Indica si el objeto ha sido guardado en el servicio remoto.
30
40
  attr_accessor :persisted
31
41
 
42
+ # @return [String, nil] Routing Key capturada en el momento de la instanciación.
43
+ attr_accessor :routing_key
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
50
+
32
51
  class << self
33
- attr_writer :connection_pool, :exchange, :exchange_type, :resource_name, :routing_key
52
+ # Configuración heredable
53
+ attr_writer :connection_pool, :exchange, :exchange_type, :resource_name, :routing_key, :param_key
34
54
 
35
- # Resuelve la configuración buscando en la jerarquía de clases.
55
+ # Lee la configuración del Thread actual (usado por el scope .with).
56
+ # @api private
57
+ def thread_config(key)
58
+ Thread.current["bb_#{object_id}_#{key}"]
59
+ end
60
+
61
+ # Resuelve la configuración buscando en: 1. Thread (Scope), 2. Clase, 3. Herencia.
36
62
  # @api private
37
63
  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)
64
+ # 1. Prioridad: Contexto de hilo (.with)
65
+ val = thread_config(key)
66
+ return val if val
67
+
68
+ # 2. Prioridad: Jerarquía de clases
40
69
  target = self
41
70
  while target <= BugBunny::Resource
42
71
  value = target.instance_variable_get(instance_var)
@@ -46,15 +75,32 @@ module BugBunny
46
75
  nil
47
76
  end
48
77
 
78
+ # @return [ConnectionPool] El pool de conexiones asignado.
49
79
  def connection_pool; resolve_config(:pool, :@connection_pool); end
80
+
81
+ # @return [String] El exchange configurado.
82
+ # @raise [ArgumentError] Si no se ha definido un exchange.
50
83
  def current_exchange; resolve_config(:exchange, :@exchange) || raise(ArgumentError, "Exchange not defined"); end
84
+
85
+ # @return [String] El tipo de exchange (default: direct).
51
86
  def current_exchange_type; resolve_config(:exchange_type, :@exchange_type) || 'direct'; end
52
87
 
88
+ # @return [String] El nombre del recurso (ej: 'users'). Se infiere del nombre de la clase si no existe.
53
89
  def resource_name
54
90
  resolve_config(:resource_name, :@resource_name) || name.demodulize.underscore.pluralize
55
91
  end
56
92
 
57
- # Define middlewares para el cliente de este recurso.
93
+ # Define la clave raíz para envolver el payload JSON (Wrapping).
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.
99
+ def param_key
100
+ resolve_config(:param_key, :@param_key) || model_name.element
101
+ end
102
+
103
+ # Define un middleware para el cliente HTTP/AMQP de este recurso.
58
104
  def client_middleware(&block)
59
105
  @client_middleware_stack ||= []
60
106
  @client_middleware_stack << block
@@ -72,56 +118,72 @@ module BugBunny
72
118
  stack
73
119
  end
74
120
 
121
+ # Instancia un cliente configurado con el pool y middlewares del recurso.
122
+ # @return [BugBunny::Client]
75
123
  def bug_bunny_client
76
124
  pool = connection_pool
77
125
  raise BugBunny::Error, "Connection pool missing for #{name}" unless pool
78
-
126
+
79
127
  BugBunny::Client.new(pool: pool) do |conn|
80
128
  resolve_middleware_stack.each { |block| block.call(conn) }
81
129
  end
82
130
  end
83
131
 
132
+ # Ejecuta un bloque (o retorna un Proxy) con una configuración temporal.
133
+ # Útil para cambiar de exchange o routing_key para una operación específica.
134
+ #
135
+ # @example
136
+ # User.with(routing_key: 'urgent').create(params)
84
137
  def with(exchange: nil, routing_key: nil, exchange_type: nil, pool: nil)
85
138
  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
139
  old_values = {}
87
140
  keys.each { |k, v| old_values[k] = Thread.current[v] }
141
+
142
+ # Seteamos valores temporales
88
143
  Thread.current[keys[:exchange]] = exchange if exchange
89
144
  Thread.current[keys[:exchange_type]] = exchange_type if exchange_type
90
145
  Thread.current[keys[:pool]] = pool if pool
91
146
  Thread.current[keys[:routing_key]] = routing_key if routing_key
147
+
92
148
  if block_given?
93
149
  begin; yield; ensure; keys.each { |k, v| Thread.current[v] = old_values[k] }; end
94
150
  else
95
151
  ScopeProxy.new(self, keys, old_values)
96
152
  end
97
153
  end
98
-
154
+
155
+ # Proxy para permitir encadenamiento: User.with(...).find(1)
99
156
  class ScopeProxy < BasicObject
100
157
  def initialize(target, keys, old_values); @target = target; @keys = keys; @old_values = old_values; end
101
158
  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
159
  end
103
160
 
104
161
  # Calcula la Routing Key.
105
- # @note En REST, por defecto es el resource_name (Topic 'users'), pero puede ser forzada.
162
+ # @return [String]
106
163
  def calculate_routing_key(id = nil)
107
- manual_rk = Thread.current["bb_#{object_id}_routing_key"]
164
+ # 1. Contexto .with
165
+ manual_rk = thread_config(:routing_key)
108
166
  return manual_rk if manual_rk
167
+
168
+ # 2. Configuración estática
109
169
  static_rk = resolve_config(:routing_key, :@routing_key)
110
170
  return static_rk if static_rk.present?
171
+
172
+ # 3. Default: Resource name
111
173
  resource_name
112
174
  end
113
175
 
114
- # @!group Acciones CRUD RESTful
176
+ # @!group Acciones CRUD RESTful (Clase)
115
177
 
116
- # GET resource?query
117
- #
118
- # @param filters [Hash] Parámetros de consulta (Query params).
178
+ # Busca recursos que coincidan con los filtros.
179
+ # Envía: GET resource?query
119
180
  def where(filters = {})
120
181
  rk = calculate_routing_key
121
182
  path = resource_name
122
- path += "?#{URI.encode_www_form(filters)}" if filters.present?
123
183
 
124
- # REST: GET collection
184
+ # Usamos Rack para serializar anidamiento (q[service]=val)
185
+ path += "?#{Rack::Utils.build_nested_query(filters)}" if filters.present?
186
+
125
187
  response = bug_bunny_client.request(path, method: :get, exchange: current_exchange, exchange_type: current_exchange_type, routing_key: rk)
126
188
 
127
189
  return [] unless response['body'].is_a?(Array)
@@ -135,18 +197,16 @@ module BugBunny
135
197
 
136
198
  def all; where({}); end
137
199
 
138
- # GET resource/id
139
- #
140
- # @param id [String, Integer] ID del recurso.
200
+ # Busca un recurso por ID.
201
+ # Envía: GET resource/id
141
202
  def find(id)
142
203
  rk = calculate_routing_key(id)
143
204
  path = "#{resource_name}/#{id}"
144
205
 
145
- # REST: GET member
146
206
  response = bug_bunny_client.request(path, method: :get, exchange: current_exchange, exchange_type: current_exchange_type, routing_key: rk)
147
207
 
148
208
  return nil if response.nil? || response['status'] == 404
149
-
209
+
150
210
  attributes = response['body']
151
211
  return nil unless attributes.is_a?(Hash)
152
212
 
@@ -156,6 +216,7 @@ module BugBunny
156
216
  instance
157
217
  end
158
218
 
219
+ # Crea un nuevo recurso.
159
220
  def create(payload)
160
221
  instance = new(payload)
161
222
  instance.save
@@ -165,36 +226,64 @@ module BugBunny
165
226
 
166
227
  # @!group Instancia
167
228
 
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
-
229
+ # Inicializa una nueva instancia del recurso.
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.
173
237
  def initialize(attributes = {})
174
238
  @remote_attributes = {}.with_indifferent_access
175
239
  @persisted = false
240
+
241
+ # === CAPTURA DE CONTEXTO ===
242
+ @routing_key = self.class.thread_config(:routing_key)
243
+ @exchange = self.class.thread_config(:exchange)
244
+ @exchange_type = self.class.thread_config(:exchange_type)
245
+
176
246
  assign_attributes(attributes)
177
247
  super()
178
248
  end
179
249
 
250
+ # Prioridad Routing Key: 1. Instancia (Capturada), 2. Clase
251
+ def calculate_routing_key(id=nil)
252
+ return @routing_key if @routing_key
253
+ self.class.calculate_routing_key(id)
254
+ end
255
+
256
+ # Prioridad Exchange: 1. Instancia (Capturada), 2. Clase
257
+ def current_exchange
258
+ @exchange || self.class.current_exchange
259
+ end
260
+
261
+ # Prioridad Exchange Type: 1. Instancia (Capturada), 2. Clase
262
+ def current_exchange_type
263
+ @exchange_type || self.class.current_exchange_type
264
+ end
265
+
266
+ def bug_bunny_client; self.class.bug_bunny_client; end
267
+
180
268
  def persisted?; !!@persisted; end
181
269
 
182
270
  def assign_attributes(new_attributes)
183
271
  return if new_attributes.nil?
184
272
  new_attributes.each { |k, v| public_send("#{k}=", v) }
185
273
  end
186
-
274
+
187
275
  def update(attributes)
188
276
  assign_attributes(attributes)
189
277
  save
190
278
  end
191
279
 
280
+ # Retorna solo los atributos que han cambiado.
192
281
  def changes_to_send
193
282
  return changes.transform_values(&:last) unless changes.empty?
194
283
  @remote_attributes.except('id', 'ID', 'Id', '_id')
195
284
  end
196
285
 
197
- # Magic Methods para atributos dinámicos
286
+ # Métodos mágicos para atributos.
198
287
  def method_missing(method_name, *args, &block)
199
288
  attribute_name = method_name.to_s
200
289
  if attribute_name.end_with?('=')
@@ -225,23 +314,36 @@ module BugBunny
225
314
 
226
315
  # @!group Persistencia RESTful
227
316
 
228
- # Guarda el registro (POST si es nuevo, PUT si existe).
317
+ # Guarda el registro.
318
+ # Envía POST si es nuevo, PUT si ya existe.
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.
229
324
  def save
230
325
  return false unless valid?
231
326
 
232
327
  run_callbacks(:save) do
233
328
  is_new = !persisted?
234
329
  rk = calculate_routing_key(id)
235
-
236
- # Mapeo a verbos HTTP usando client.request(method: ...)
330
+
331
+ # 1. Obtenemos el payload plano (atributos modificados)
332
+ flat_payload = changes_to_send
333
+
334
+ # 2. Wrappeamos automáticamente en la clave del modelo
335
+ key = self.class.param_key
336
+ wrapped_payload = { key => flat_payload }
337
+
338
+ # Mapeo a verbos HTTP
237
339
  if is_new
238
340
  # REST: POST resource (Create)
239
341
  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)
342
+ response = bug_bunny_client.request(path, method: :post, exchange: current_exchange, exchange_type: current_exchange_type, routing_key: rk, body: wrapped_payload)
241
343
  else
242
344
  # REST: PUT resource/id (Update)
243
345
  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)
346
+ response = bug_bunny_client.request(path, method: :put, exchange: current_exchange, exchange_type: current_exchange_type, routing_key: rk, body: wrapped_payload)
245
347
  end
246
348
 
247
349
  handle_save_response(response)
@@ -251,12 +353,12 @@ module BugBunny
251
353
  false
252
354
  end
253
355
 
254
- # Elimina el registro (DELETE).
356
+ # Elimina el registro.
357
+ # Envía DELETE resource/id.
255
358
  def destroy
256
359
  return false unless persisted?
257
360
 
258
361
  run_callbacks(:destroy) do
259
- # REST: DELETE resource/id
260
362
  path = "#{self.class.resource_name}/#{id}"
261
363
  rk = calculate_routing_key(id)
262
364
 
@@ -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.3"
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,
data/test_resource.rb CHANGED
@@ -1,22 +1,19 @@
1
1
  require_relative 'test_helper'
2
2
 
3
3
  class TestUser < BugBunny::Resource
4
- # Se decide qué pool usar en cada petición
5
- self.connection_pool = -> {
6
- nil || TEST_POOL
7
- }
8
-
9
- # El exchange cambia según el entorno
10
- self.exchange = -> {
11
- ENV['IS_STAGING'] ? 'test_exchange' : 'test_exchange'
12
- }
4
+ # Configuración del Pool
5
+ self.connection_pool = -> { nil || TEST_POOL }
13
6
 
7
+ # Configuración del Exchange
8
+ self.exchange = -> { ENV['IS_STAGING'] ? 'test_exchange' : 'test_exchange' }
14
9
  self.exchange_type = 'topic'
15
- self.routing_key_prefix = 'test_user'
16
10
 
17
- attribute :id, :integer
18
- attribute :name, :string
19
- attribute :email, :string
11
+ # ACTUALIZADO v3.0: Usamos resource_name
12
+ self.resource_name = 'test_users'
13
+
14
+ # ELIMINADO: attribute :id, :integer (Causa crash en v3)
15
+ # ELIMINADO: attribute :name, :string (Causa crash en v3)
20
16
 
17
+ # Las validaciones siguen funcionando igual
21
18
  validates :name, presence: true
22
19
  end
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.3
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-13 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: []