Class: BugBunny::Resource

Inherits:
Object
  • Object
show all
Extended by:
ActiveModel::Callbacks
Includes:
ActiveModel::API, ActiveModel::Attributes, ActiveModel::Dirty, ActiveModel::Validations
Defined in:
lib/bug_bunny/resource.rb

Overview

Clase base para modelos remotos que implementan **Active Record over AMQP (RESTful)**.

Soporta un esquema híbrido de datos y configuración de infraestructura en cascada:

  1. Defaults: Definidos en la sesión.

  2. Global: Definidos en BugBunny.configuration.

  3. **Específico:** Definidos en la clase del recurso o vía ‘with`.

Author:

  • Gabriel

Since:

  • 3.1.2

Defined Under Namespace

Classes: ScopeProxy

Configuración de Infraestructura Específica collapse

Class Attribute Summary collapse

Instance Attribute Summary collapse

Configuración de Infraestructura Específica collapse

Acciones CRUD RESTful collapse

Instancia collapse

Persistencia collapse

Constructor Details

#initialize(attributes = {}) ⇒ Resource

Inicializa el recurso.

Parameters:

  • attributes (Hash) (defaults to: {})

Since:

  • 3.1.2



283
284
285
286
287
288
289
290
291
292
293
294
295
296
# File 'lib/bug_bunny/resource.rb', line 283

def initialize(attributes = {})
  @extra_attributes = {}.with_indifferent_access
  @dynamic_changes = Set.new
  @persisted = false

  # Contexto de infraestructura
  @routing_key = self.class.thread_config(:routing_key)
  @exchange = self.class.thread_config(:exchange)
  @exchange_type = self.class.thread_config(:exchange_type)
  @exchange_options = self.class.thread_config(:exchange_options) || self.class.current_exchange_options
  @queue_options = self.class.thread_config(:queue_options) || self.class.current_queue_options

  super
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method_name, *args, &block) ⇒ Object

Intercepta asignaciones dinámicas y las registra como cambios.

Since:

  • 3.1.2



380
381
382
383
384
385
386
387
388
389
390
391
392
393
# File 'lib/bug_bunny/resource.rb', line 380

def method_missing(method_name, *args, &block)
  attribute_name = method_name.to_s
  if attribute_name.end_with?('=')
    key = attribute_name.chop
    val = args.first

    if @extra_attributes[key] != val
      @dynamic_changes << key
      @extra_attributes[key] = val
    end
  else
    @extra_attributes.key?(attribute_name) ? @extra_attributes[attribute_name] : super
  end
end

Class Attribute Details

.connection_poolConnectionPool?

Returns:

  • (ConnectionPool, nil)

Since:

  • 3.1.2



63
64
65
# File 'lib/bug_bunny/resource.rb', line 63

def connection_pool
  resolve_config(:pool, :@connection_pool)
end

.exchange=(value) ⇒ Object (writeonly)

Since:

  • 3.1.2



34
35
36
# File 'lib/bug_bunny/resource.rb', line 34

def exchange=(value)
  @exchange = value
end

.exchange_options=(value) ⇒ Object (writeonly)

Since:

  • 3.1.2



37
38
39
# File 'lib/bug_bunny/resource.rb', line 37

def exchange_options=(value)
  @exchange_options = value
end

.exchange_type=(value) ⇒ Object (writeonly)

Since:

  • 3.1.2



34
35
36
# File 'lib/bug_bunny/resource.rb', line 34

def exchange_type=(value)
  @exchange_type = value
end

.param_keyString

Returns Clave raíz para envolver el payload en las peticiones.

Returns:

  • (String)

    Clave raíz para envolver el payload en las peticiones.

Since:

  • 3.1.2



93
94
95
# File 'lib/bug_bunny/resource.rb', line 93

def param_key
  resolve_config(:param_key, :@param_key) || model_name.element
end

.queue_options=(value) ⇒ Object (writeonly)

Since:

  • 3.1.2



37
38
39
# File 'lib/bug_bunny/resource.rb', line 37

def queue_options=(value)
  @queue_options = value
end

.resource_nameString

Returns Nombre del recurso para la construcción de rutas.

Returns:

  • (String)

    Nombre del recurso para la construcción de rutas.

Since:

  • 3.1.2



88
89
90
# File 'lib/bug_bunny/resource.rb', line 88

def resource_name
  resolve_config(:resource_name, :@resource_name) || name.demodulize.underscore.pluralize
end

.routing_key=(value) ⇒ Object (writeonly)

Since:

  • 3.1.2



34
35
36
# File 'lib/bug_bunny/resource.rb', line 34

def routing_key=(value)
  @routing_key = value
end

Instance Attribute Details

#exchangeObject

Since:

  • 3.1.2



28
29
30
# File 'lib/bug_bunny/resource.rb', line 28

def exchange
  @exchange
end

#exchange_optionsHash

Returns Opciones específicas de instancia para exchange y queue.

Returns:

  • (Hash)

    Opciones específicas de instancia para exchange y queue.

Since:

  • 3.1.2



31
32
33
# File 'lib/bug_bunny/resource.rb', line 31

def exchange_options
  @exchange_options
end

#exchange_typeObject

Since:

  • 3.1.2



28
29
30
# File 'lib/bug_bunny/resource.rb', line 28

def exchange_type
  @exchange_type
end

#persistedObject

Since:

  • 3.1.2



28
29
30
# File 'lib/bug_bunny/resource.rb', line 28

def persisted
  @persisted
end

#queue_optionsHash

Returns Opciones específicas de instancia para exchange y queue.

Returns:

  • (Hash)

    Opciones específicas de instancia para exchange y queue.

Since:

  • 3.1.2



31
32
33
# File 'lib/bug_bunny/resource.rb', line 31

def queue_options
  @queue_options
end

#remote_attributesObject (readonly)

Since:

  • 3.1.2



27
28
29
# File 'lib/bug_bunny/resource.rb', line 27

def remote_attributes
  @remote_attributes
end

#routing_keyObject

Since:

  • 3.1.2



28
29
30
# File 'lib/bug_bunny/resource.rb', line 28

def routing_key
  @routing_key
end

Class Method Details

.allArray<BugBunny::Resource>

Devuelve todos los registros.

Returns:

Since:

  • 3.1.2



236
237
238
# File 'lib/bug_bunny/resource.rb', line 236

def all
  where({})
end

.bug_bunny_clientBugBunny::Client

Instancia el cliente inyectando los middlewares núcleo y personalizados. Integra automáticamente ‘RaiseError` y `JsonResponse` para que el ORM trabaje puramente con datos parseados o atrape excepciones sin validar HTTP Status manuales.

Returns:

Raises:

Since:

  • 3.1.2



120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/bug_bunny/resource.rb', line 120

def bug_bunny_client
  pool = connection_pool
  raise BugBunny::Error, "Connection pool missing for #{name}" unless pool

  BugBunny::Client.new(pool: pool) do |stack|
    # 1. Middlewares Core (Siempre presentes para el Resource)
    stack.use BugBunny::Middleware::RaiseError
    stack.use BugBunny::Middleware::JsonResponse

    # 2. Middlewares Personalizados del Usuario
    resolve_middleware_stack.each { |block| block.call(stack) }
  end
end

.calculate_routing_key(_id = nil) ⇒ String

Calcula la routing key final.

Parameters:

  • id (String, nil)

    ID del recurso.

Returns:

  • (String)

Since:

  • 3.1.2



191
192
193
194
195
196
197
198
199
# File 'lib/bug_bunny/resource.rb', line 191

def calculate_routing_key(_id = nil)
  manual_rk = thread_config(:routing_key)
  return manual_rk if manual_rk

  static_rk = resolve_config(:routing_key, :@routing_key)
  return static_rk if static_rk.present?

  resource_name
end

.client_middleware(&block) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Since:

  • 3.1.2



98
99
100
101
# File 'lib/bug_bunny/resource.rb', line 98

def client_middleware(&block)
  @client_middleware_stack ||= []
  @client_middleware_stack << block
end

.create(payload) ⇒ BugBunny::Resource

Crea una nueva instancia y la persiste.

Parameters:

  • payload (Hash)

Returns:

Since:

  • 3.1.2



272
273
274
275
276
# File 'lib/bug_bunny/resource.rb', line 272

def create(payload)
  instance = new(payload)
  instance.save
  instance
end

.current_exchangeString

Returns Nombre del exchange actual.

Returns:

  • (String)

    Nombre del exchange actual.

Since:

  • 3.1.2



68
69
70
# File 'lib/bug_bunny/resource.rb', line 68

def current_exchange
  resolve_config(:exchange, :@exchange) || raise(ArgumentError, "Exchange not defined for #{name}")
end

.current_exchange_optionsHash

Returns Opciones de exchange específicas (Nivel 3 de la cascada).

Returns:

  • (Hash)

    Opciones de exchange específicas (Nivel 3 de la cascada).

Since:

  • 3.1.2



78
79
80
# File 'lib/bug_bunny/resource.rb', line 78

def current_exchange_options
  resolve_config(:exchange_options, :@exchange_options) || {}
end

.current_exchange_typeString

Returns Tipo de exchange (‘direct’, ‘topic’, ‘fanout’).

Returns:

  • (String)

    Tipo de exchange (‘direct’, ‘topic’, ‘fanout’).

Since:

  • 3.1.2



73
74
75
# File 'lib/bug_bunny/resource.rb', line 73

def current_exchange_type
  resolve_config(:exchange_type, :@exchange_type) || 'direct'
end

.current_queue_optionsHash

Returns Opciones de cola específicas.

Returns:

  • (Hash)

    Opciones de cola específicas.

Since:

  • 3.1.2



83
84
85
# File 'lib/bug_bunny/resource.rb', line 83

def current_queue_options
  resolve_config(:queue_options, :@queue_options) || {}
end

.find(id) ⇒ BugBunny::Resource?

Busca un registro por ID (GET). Mapea un 404 (NotFound) devolviendo un objeto nulo.

Parameters:

  • id (String, Integer)

Returns:

Since:

  • 3.1.2



245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# File 'lib/bug_bunny/resource.rb', line 245

def find(id)
  rk = calculate_routing_key(id)
  path = "#{resource_name}/#{id}"

  response = bug_bunny_client.request(
    path,
    method: :get,
    exchange: current_exchange,
    exchange_type: current_exchange_type,
    routing_key: rk,
    exchange_options: current_exchange_options,
    queue_options: current_queue_options
  )

  return nil unless response && response['body'].is_a?(Hash)

  instance = new(response['body'])
  instance.persisted = true
  instance.send(:clear_changes_information)
  instance
rescue BugBunny::NotFound
  nil
end

.resolve_config(key, instance_var) ⇒ Object?

Resuelve la configuración buscando en el hilo, luego en la jerarquía de clases.

Parameters:

  • key (Symbol)

    Clave en el Thread.current.

  • instance_var (Symbol)

    Nombre de la variable de instancia en la clase.

Returns:

  • (Object, nil)

Since:

  • 3.1.2



48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/bug_bunny/resource.rb', line 48

def resolve_config(key, instance_var)
  val = thread_config(key)
  return val if val

  target = self
  while target <= BugBunny::Resource
    value = target.instance_variable_get(instance_var)
    return value.respond_to?(:call) ? value.call : value unless value.nil?

    target = target.superclass
  end
  nil
end

.resolve_middleware_stackObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Since:

  • 3.1.2



104
105
106
107
108
109
110
111
112
113
# File 'lib/bug_bunny/resource.rb', line 104

def resolve_middleware_stack
  stack = []
  target = self
  while target <= BugBunny::Resource
    middlewares = target.instance_variable_get(:@client_middleware_stack)
    stack.unshift(*middlewares) if middlewares
    target = target.superclass
  end
  stack
end

.thread_config(key) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Since:

  • 3.1.2



40
41
42
# File 'lib/bug_bunny/resource.rb', line 40

def thread_config(key)
  Thread.current["bb_#{object_id}_#{key}"]
end

.where(filters = {}) ⇒ Array<BugBunny::Resource>

Realiza una búsqueda filtrada (GET). Mapea un posible 404 a un array vacío.

Parameters:

  • filters (Hash) (defaults to: {})

Returns:

Since:

  • 3.1.2



208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/bug_bunny/resource.rb', line 208

def where(filters = {})
  rk = calculate_routing_key

  response = bug_bunny_client.request(
    resource_name,
    method: :get,
    exchange: current_exchange,
    exchange_type: current_exchange_type,
    routing_key: rk,
    exchange_options: current_exchange_options,
    queue_options: current_queue_options,
    params: filters.presence || {}
  )

  return [] unless response['body'].is_a?(Array)

  response['body'].map do |attrs|
    inst = new(attrs)
    inst.persisted = true
    inst.send(:clear_changes_information)
    inst
  end
rescue BugBunny::NotFound
  []
end

.with(exchange: nil, routing_key: nil, exchange_type: nil, pool: nil, exchange_options: nil, queue_options: nil) ⇒ Object

Permite configurar dinámicamente el contexto AMQP para una operación.

Parameters:

  • exchange (String) (defaults to: nil)

    Nombre del exchange.

  • routing_key (String) (defaults to: nil)

    Routing key manual.

  • exchange_type (String) (defaults to: nil)

    Tipo de exchange.

  • pool (ConnectionPool) (defaults to: nil)

    Pool de conexiones.

  • exchange_options (Hash) (defaults to: nil)

    Opciones de infraestructura.

  • queue_options (Hash) (defaults to: nil)

    Opciones de cola.

Since:

  • 3.1.2



142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/bug_bunny/resource.rb', line 142

def with(exchange: nil, routing_key: nil, exchange_type: nil, pool: nil, exchange_options: nil,
         queue_options: nil)
  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",
    exchange_options: "bb_#{object_id}_exchange_options",
    queue_options: "bb_#{object_id}_queue_options"
  }
  old_values = {}
  keys.each { |k, v| old_values[k] = Thread.current[v] }

  Thread.current[keys[:exchange]] = exchange if exchange
  Thread.current[keys[:exchange_type]] = exchange_type if exchange_type
  Thread.current[keys[:pool]] = pool if pool
  Thread.current[keys[:routing_key]] = routing_key if routing_key
  Thread.current[keys[:exchange_options]] = exchange_options if exchange_options
  Thread.current[keys[:queue_options]] = queue_options if queue_options

  if block_given?
    begin; yield; ensure; keys.each { |k, v| Thread.current[v] = old_values[k] }; end
  else
    ScopeProxy.new(self, keys, old_values)
  end
end

Instance Method Details

#assign_attributes(new_attributes) ⇒ Object

Asignación masiva de atributos.

Parameters:

  • new_attributes (Hash)

Since:

  • 3.1.2



347
348
349
350
351
# File 'lib/bug_bunny/resource.rb', line 347

def assign_attributes(new_attributes)
  return if new_attributes.nil?

  new_attributes.each { |k, v| public_send("#{k}=", v) }
end

#attributes_for_serializationHash

Serialización combinada.

Returns:

  • (Hash)

Since:

  • 3.1.2



316
317
318
# File 'lib/bug_bunny/resource.rb', line 316

def attributes_for_serialization
  @extra_attributes.merge(attributes)
end

#bug_bunny_clientBugBunny::Client

Returns:

Since:

  • 3.1.2



336
337
338
# File 'lib/bug_bunny/resource.rb', line 336

def bug_bunny_client
  self.class.bug_bunny_client
end

#calculate_routing_key(id = nil) ⇒ String

Returns:

  • (String)

Since:

  • 3.1.2



321
322
323
# File 'lib/bug_bunny/resource.rb', line 321

def calculate_routing_key(id = nil)
  @routing_key || self.class.calculate_routing_key(id)
end

#changedArray<String>

Returns Lista de atributos que han cambiado.

Returns:

  • (Array<String>)

    Lista de atributos que han cambiado.

Since:

  • 3.1.2



310
311
312
# File 'lib/bug_bunny/resource.rb', line 310

def changed
  (super + @dynamic_changes.to_a).uniq
end

#changed?Boolean

Returns true si hay cambios nativos o dinámicos.

Returns:

  • (Boolean)

    true si hay cambios nativos o dinámicos.

Since:

  • 3.1.2



305
306
307
# File 'lib/bug_bunny/resource.rb', line 305

def changed?
  super || @dynamic_changes.any?
end

#changes_to_sendHash

Retorna el hash combinado de cambios (Tipados + Dinámicos).

Returns:

  • (Hash)

Since:

  • 3.1.2



363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
# File 'lib/bug_bunny/resource.rb', line 363

def changes_to_send
  # 1. Obtener los nombres de todos los atributos que han cambiado (incluyendo dinámicos vía attribute_will_change!)
  changed_keys = changed

  # 2. Construir el payload con los valores actuales de esas keys
  payload = {}
  changed_keys.each do |key|
    payload[key] = public_send(key)
  end

  return payload unless payload.empty?

  # Fallback: Si no hay cambios detectados (ej: en un create), enviamos todo
  attributes_for_serialization.except('id', 'ID', 'Id', '_id')
end

#clear_changes_informationObject

Limpia el rastreo de ActiveModel y nuestro rastreo dinámico interno.

Since:

  • 3.1.2



299
300
301
302
# File 'lib/bug_bunny/resource.rb', line 299

def clear_changes_information
  super
  @dynamic_changes.clear
end

#current_exchangeString

Returns:

  • (String)

Since:

  • 3.1.2



326
327
328
# File 'lib/bug_bunny/resource.rb', line 326

def current_exchange
  @exchange || self.class.current_exchange
end

#current_exchange_typeString

Returns:

  • (String)

Since:

  • 3.1.2



331
332
333
# File 'lib/bug_bunny/resource.rb', line 331

def current_exchange_type
  @exchange_type || self.class.current_exchange_type
end

#destroyBoolean

Elimina el recurso del servidor remoto (DELETE).

Returns:

  • (Boolean)

Since:

  • 3.1.2



473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
# File 'lib/bug_bunny/resource.rb', line 473

def destroy
  return false unless persisted?
  return false unless id

  run_callbacks(:destroy) do
    path = "#{self.class.resource_name}/#{id}"
    rk = calculate_routing_key(id)

    bug_bunny_client.request(
      path,
      method: :delete,
      exchange: current_exchange,
      exchange_type: current_exchange_type,
      routing_key: rk,
      exchange_options: @exchange_options,
      queue_options: @queue_options
    )

    self.persisted = false
  end
  true
rescue BugBunny::UnprocessableEntity => e
  load_remote_rabbit_errors(e.error_messages)
  false
rescue BugBunny::ClientError => e
  load_remote_rabbit_errors(e.message)
  false
rescue BugBunny::ServerError
  false
end

#idObject

Returns Valor del ID buscando en múltiples nomenclaturas.

Returns:

  • (Object)

    Valor del ID buscando en múltiples nomenclaturas.

Since:

  • 3.1.2



400
401
402
# File 'lib/bug_bunny/resource.rb', line 400

def id
  attributes['id'] || @extra_attributes['id'] || @extra_attributes['ID'] || @extra_attributes['Id'] || @extra_attributes['_id']
end

#id=(value) ⇒ Object

Since:

  • 3.1.2



415
416
417
418
419
420
421
422
# File 'lib/bug_bunny/resource.rb', line 415

def id=(value)
  if self.class.attribute_names.include?('id')
    super
  else
    @dynamic_changes << 'id' if @extra_attributes['id'] != value
    @extra_attributes['id'] = value
  end
end

#inspectString

Representación legible del recurso. Muestra solo ID y atributos principales, sin detalles de infraestructura.

Returns:

  • (String)

Since:

  • 3.1.2



408
409
410
411
412
413
# File 'lib/bug_bunny/resource.rb', line 408

def inspect
  infra_keys = %w[routing_key exchange exchange_type exchange_options queue_options _id]
  attrs = @extra_attributes.merge(attributes).reject { |k, _| infra_keys.include?(k) || k == 'id' }
  attr_str = attrs.first(5).map { |k, v| "#{k}=#{v.inspect}" }.join(' ')
  "#<#{self.class.name} id=#{id.inspect} persisted=#{@persisted}#{" #{attr_str}" unless attr_str.empty?}>"
end

#persisted?Boolean

Returns:

  • (Boolean)

Since:

  • 3.1.2



341
342
343
# File 'lib/bug_bunny/resource.rb', line 341

def persisted?
  !!@persisted
end

#read_attribute_for_validation(attr) ⇒ Object

Since:

  • 3.1.2



424
425
426
427
# File 'lib/bug_bunny/resource.rb', line 424

def read_attribute_for_validation(attr)
  attr_s = attr.to_s
  self.class.attribute_names.include?(attr_s) ? attribute(attr_s) : @extra_attributes[attr_s]
end

#respond_to_missing?(method_name, include_private = false) ⇒ Boolean

Returns:

  • (Boolean)

Since:

  • 3.1.2



395
396
397
# File 'lib/bug_bunny/resource.rb', line 395

def respond_to_missing?(method_name, include_private = false)
  @extra_attributes.key?(method_name.to_s.sub(/=$/, '')) || super
end

#saveBoolean

Guarda el recurso en el servidor remoto vía AMQP (POST o PUT). Asume el Happy Path; el middleware se encarga de interceptar y lanzar excepciones.

Returns:

  • (Boolean)

    Retorna true si tuvo éxito, false si falló la validación.

Since:

  • 3.1.2



435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
# File 'lib/bug_bunny/resource.rb', line 435

def save
  return false unless valid?

  run_callbacks(:save) do
    is_new = !persisted?
    rk = calculate_routing_key(id)
    flat_payload = changes_to_send
    key = self.class.param_key
    wrapped_payload = { key => flat_payload }

    path = is_new ? self.class.resource_name : "#{self.class.resource_name}/#{id}"
    method = is_new ? :post : :put

    # Si el middleware de errores no lanza excepción, asumimos un éxito (200..299)
    response = bug_bunny_client.request(
      path,
      method: method,
      exchange: current_exchange,
      exchange_type: current_exchange_type,
      routing_key: rk,
      exchange_options: @exchange_options,
      queue_options: @queue_options,
      body: wrapped_payload
    )

    assign_attributes(response['body'])
    self.persisted = true
    clear_changes_information
    true
  end
rescue BugBunny::UnprocessableEntity => e
  load_remote_rabbit_errors(e.error_messages)
  false
end

#update(attributes) ⇒ Boolean

Actualiza y guarda.

Parameters:

  • attributes (Hash)

Returns:

  • (Boolean)

Since:

  • 3.1.2



356
357
358
359
# File 'lib/bug_bunny/resource.rb', line 356

def update(attributes)
  assign_attributes(attributes)
  save
end