En esta idea de “Rails is not my app” es necesario definir cuales son las cosas que va hacer tu aplicación y cuales va a delegar.
Cuando usas Rails o algún otro framework similar, lo usas porque no quieres repetir lo que otros ya han hecho, para así desarrollar más rápido. Pero si estas tratando de separa la “lógica de tu aplicación” del framework no es tan fácil.
Desde mi punto de vista las cosas más grandes o importantes que nos da ActiveRecord es la validación de datos y la comunicación con la base de datos. ¿Como puedes hacer para seguir aprovechando esto?
Lo que yo hago es tratar de considerar al modelo como si fuera mi fuente de almacenamiento de datos. Algo muy similar a lo que llaman “Repository Pattern”, pero siempre inyectando la dependencia.
Por ejemplo, en la siguiente función para exportar usuarios, tengo que pedir los últimos usuarios registrados. Para esto inyecto la clase User de Active Record que viene siendo mi “Users Repository” y para las pruebas creo un objeto muy simple con el que puedo definir la interfaz esperada.
class User < ActiveRecord::Base
def self.last_registered(number)
order(id: :desc).limit(number)
end
end
class Admin::UsersExportsController < Admin::BaseController
def create
export = Users.export_users(User, params[:users_export], Date.current)
# other stuff...
end
end
module Users
def self.export_users(store, params, current_date)
# stuff..
records = store.last_registered(form.number_of_users)
# stuff..
end
end
module Users
RSpec.describe "Export users" do
describe "generates a list of users" do
it "with the given number of last registered users" do
store = store_with (1..100).map { |id| user_with(id: id) }
export = export_users(store, "number_of_users" => 10)
expect(export.users_list.count).to eq 10
expect(export.users_list.first.id).to eq 100
end
end
def store_with(users)
FakeUsersStore.new(users)
end
def user_with(attrs)
FakeUser.new(attrs)
end
def export_users(store, params, current_date = Date.current)
Users.export_users(store, params, current_date)
end
class FakeUsersStore
def initialize(records)
@records = records
end
def last_registered(number)
records.reverse.first(number)
end
private
attr_reader :records
end
end
end
La mayoría de las validaciones de un modelo vienen definidas en el módulo ActiveModel::Model. Por lo que una forma de seguir utilizando estos métodos, es incluir ActiveModel como dependencia.
Por ejemplo, en la siguiente función ProjectUpdates.createprojectupdate incluyo ActiveModel::Model en ProjectUpdates::Form para que pueda hacer las validaciones por mi.
class Admin::ProjectUpdatesController < Admin::BaseController
def create
project = Project.find_by_slug!(params[:project_id])
creation = ProjectUpdates.create_project_update(project, params[:project_update], ProjectUpdate)
if creation.success?
# render something
else
# render something
end
end
end
module ProjectUpdates
def self.create_project_update(project, params, project_updates_store)
CreateProjectUpdate.new(project, params, project_updates_store).call
end
class CreateProjectUpdate
def initialize(project, params, project_updates_store)
@project = project
@form = Form.new(params)
@project_updates_store = project_updates_store
end
def call
if form.valid?
record = project_updates_store.create(form.to_h.merge(project_id: project.id))
SuccessResponse.new(record)
else
ErrorResponse.new(NewUpdatePage.new(project, form))
end
end
private
attr_reader :project, :form, :project_updates_store, :notifier
end
class Form
include ActiveModel::Model
ATTRIBUTES = [:title, :content, :date]
attr_accessor *ATTRIBUTES
validates_presence_of *ATTRIBUTES
def to_h
ATTRIBUTES.map { |field| [field, send(field)] }.to_h
end
end
end
module ProjectUpdates
RSpec.describe "Create project updates" do
describe "creates a record" do
describe "(error)" do
before do
expect(project_updates_store).not_to receive(:create)
end
it "validates presence of title" do
creation = create_project_update(project, params.merge("title" => nil))
expect(creation).not_to be_success
expect(creation.page.form.errors[:title]).to eq ["no puede estar en blanco"]
end
it "validates presence of content" do
creation = create_project_update(project, params.merge("content" => nil))
expect(creation).not_to be_success
expect(creation.page.form.errors[:content]).to eq ["no puede estar en blanco"]
end
it "validates presence of date" do
creation = create_project_update(project, params.merge("date" => nil))
expect(creation).not_to be_success
expect(creation.page.form.errors[:date]).to eq ["no puede estar en blanco"]
end
end
end
end
end
…Espero que estos dos ejemplos o técnicas te ayuden a visualizar un poco como puedes tratar de separar la lógica de tu aplicación de Rails o el framework que estas usando.
I send an email each week, trying to share knowledge and fixes to common problems and struggles for ruby on rails developers, like How to fetch the latest-N-of-each record or How to test that an specific mail was sent or a Capybara cheatsheet. You can see more examples on Most recent posts or All post by topic.