Ruby on Rails es mi framework principal desde hace años, y proyecto tras proyecto me descubría reescribiendo los mismos patrones de memoria. Rails Notes es mi respuesta: una biblioteca de componentes probados en producción (servicios, jobs, Mongoid, Kafka) lista para llevar al siguiente proyecto.
Ingenieria
Rails 7, Ruby 3.4, MongoDB, Mongoid, Kafka, Sidekiq, Redis, RSpec, Docker
Ruby on Rails es mi framework principal desde hace muchos años, con él he construido desde plataformas de coleccionables deportivos hasta backends de alto tráfico. La razón es aburrida y por eso buena: sus convenciones deciden por ti las mil pequeñas cosas que no importan, y a un equipo pequeño (o de uno) eso le regala una productividad que pocos frameworks igualan. Pero ningún framework te libra de tus propias repeticiones: de eso va esta biblioteca.
Cuando programas muchos proyectos con el mismo framework, te pasa algo curioso: no reescribes lo difícil, reescribes lo de siempre. El servicio con su patrón de llamada, el enum que Mongoid no trae, la espera de un lote de jobs de Sidekiq. Y reescribir de memoria tiene un coste escondido: cada proyecto acaba con su versión ligeramente distinta del mismo patrón, con sus bugs ligeramente distintos.
Un repositorio-biblioteca: componentes reales, extraídos de aplicaciones en producción, con sus tests y su documentación. No es una aplicación, es la caja de piezas de la que arranca cada proyecto nuevo. Tres ejemplos:
Todos mis servicios se llaman igual: call. Este pequeño concern impone el patrón: el servicio se instancia y ejecuta en una línea, y si a alguien se le olvida implementar call, explota en desarrollo, no en producción:
module Callable
extend ActiveSupport::Concern
included do
def call
raise NotImplementedError
end
end
module ClassMethods
def call(*args)
new(*args).call
end
end
end
# Uso: da igual el servicio, siempre la misma forma
InvoiceExporter.call(invoice)
Parece poca cosa. El valor está en lo que evita: veinte servicios con veinte formas de llamarse.
ActiveRecord tiene enum; Mongoid, no. En vez de simularlo distinto en cada modelo, un concern que genera constantes, accessors y métodos de conveniencia a partir de una declaración:
class Card
include Mongoid::Document
include FieldEnumerable
field :source, type: Integer
field_enum source: { marketplace: 0, battle_arena: 1 }, _prefix: true
end
# Y a partir de ahí, como en ActiveRecord:
card.source_marketplace? # => true/false
Card::SOURCE_MARKETPLACE # => 0
Lanzar cien jobs de Sidekiq es fácil; saber cuándo han terminado todos, no tanto. Un pool que registra los IDs y espera:
pool = AsyncJobPool.new
cards.each { |card| pool.add(CardRenderJob.perform_async(card.id)) }
pool.wait_for_all # bloquea hasta que el lote termina (o reporta los fallidos)
Las agregaciones de MongoDB en crudo son arrays de hashes que crecen hasta volverse ilegibles. Aggregator las convierte en una cadena de etapas componible:
# Antes: el hash crudo
Card.collection.aggregate([
{ "$match" => { "season" => season_id } },
{ "$group" => { "_id" => "$player_id", "total" => { "$sum" => 1 } } },
{ "$sort" => { "total" => -1 } }
])
# Después: etapas encadenadas, con helpers para las expresiones
Aggregate::Aggregator.new(Card)
.add_stage(:match, { season: season_id })
.add_stage(:group, { _id: "$player_id", total: { "$sum": 1 } })
.add_stage(:sort, { total: -1 })
.apply
El $ de cada etapa se normaliza solo, las expresiones comunes (in, eq, lt…) tienen helper, y el pipeline se puede construir por partes (un filtro aquí, una etapa condicional allá) antes de ejecutarlo.
Por dar una idea de lo que se acumula con los años:
AsyncJobPool: jobs en secuencia, cada uno cuando termina el anterior.Sin ceremonia: o entra la gema con la caja completa, o se copia la pieza suelta al proyecto. Cada componente lleva sus specs, que hacen de documentación ejecutable: si dudas de cómo se usa BaseSearch, lees su test y ves un ejemplo funcionando. Esa es otra regla de la biblioteca: pieza sin spec no entra.
Estos componentes viven también empaquetados en una gema propia que agrupa la caja de herramientas completa. Pero (honestidad) hay proyectos donde no monto la gema entera: copio la pieza suelta que necesito y listo. Cuarenta líneas que entiendes a veces valen más que una dependencia; la gema compensa cuando vas a usar media caja. Tener las dos opciones es justo el objetivo de esta biblioteca.
Cada proyecto Rails nuevo arranca con las piezas aburridas ya resueltas y probadas. Es la diferencia entre empezar desde cero y empezar con ventaja, y la razón de que dos proyectos míos separados por años se lean como escritos por la misma mano. Porque lo están.